aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Reck <jreck@google.com>2018-10-08 20:32:38 -0700
committerandroid-build-merger <android-build-merger@google.com>2018-10-08 20:32:38 -0700
commitd623feb17369a7dc58dcdde927cbd49ecafbb5b3 (patch)
tree49ee5735654d00a3ff7fa43a1fd460580f5b52fb
parent7f0a7b9711a6bbc8f5277908cbbc4c25814c7026 (diff)
parent842adf2777b457b504a1239963bce504fd70f5eb (diff)
downloadplatform_tools_trebuchet-d623feb17369a7dc58dcdde927cbd49ecafbb5b3.tar.gz
platform_tools_trebuchet-d623feb17369a7dc58dcdde927cbd49ecafbb5b3.tar.bz2
platform_tools_trebuchet-d623feb17369a7dc58dcdde927cbd49ecafbb5b3.zip
Initial import of trebuchet am: fe7db0b9c7 am: a870527712
am: 842adf2777 Change-Id: Ibe0951118d5bdc6db9a2a762f0a0a544c4140cd2
-rw-r--r--.gitignore23
-rw-r--r--Android.bp97
-rw-r--r--CONTRIBUTING.md23
-rw-r--r--LICENSE202
-rw-r--r--README.md5
-rw-r--r--TEST_MAPPING7
-rw-r--r--build.gradle37
-rw-r--r--core/common/build.gradle31
-rw-r--r--core/common/src/main/kotlin/trebuchet/collections/SparseArray.kt386
-rw-r--r--core/common/src/main/kotlin/trebuchet/extractors/Extractor.kt35
-rw-r--r--core/common/src/main/kotlin/trebuchet/extractors/ExtractorFactory.kt24
-rw-r--r--core/common/src/main/kotlin/trebuchet/extractors/ExtractorRegistry.kt35
-rw-r--r--core/common/src/main/kotlin/trebuchet/extractors/SystraceExtractor.kt88
-rw-r--r--core/common/src/main/kotlin/trebuchet/extractors/ZlibExtractor.kt125
-rw-r--r--core/common/src/main/kotlin/trebuchet/extras/ImportUtils.kt54
-rw-r--r--core/common/src/main/kotlin/trebuchet/extras/StreamAdapter.kt52
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ImportFeedback.kt22
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/Importer.kt31
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ImporterFactory.kt24
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ImporterRegistry.kt34
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporter.kt73
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporterState.kt104
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceLine.kt160
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/ImportData.kt36
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/events/EventParserState.kt116
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEvent.kt68
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEventRegistry.kt25
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/events/SchedEvent.kt88
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/events/TraceMarkerEvent.kt114
-rw-r--r--core/common/src/main/kotlin/trebuchet/importers/ftrace/events/WorkqueueEvent.kt50
-rw-r--r--core/common/src/main/kotlin/trebuchet/io/BufferProducer.kt25
-rw-r--r--core/common/src/main/kotlin/trebuchet/io/DataSlice.kt106
-rw-r--r--core/common/src/main/kotlin/trebuchet/io/GenericByteBuffer.kt22
-rw-r--r--core/common/src/main/kotlin/trebuchet/io/Pipe.kt59
-rw-r--r--core/common/src/main/kotlin/trebuchet/io/StreamingLineReader.kt74
-rw-r--r--core/common/src/main/kotlin/trebuchet/io/StreamingReader.kt108
-rw-r--r--core/common/src/main/kotlin/trebuchet/queries/SliceQueries.kt122
-rw-r--r--core/common/src/main/kotlin/trebuchet/queries/ThreadQueries.kt32
-rw-r--r--core/common/src/main/kotlin/trebuchet/task/ImportTask.kt73
-rw-r--r--core/common/src/main/kotlin/trebuchet/util/BatchProcessor.kt171
-rw-r--r--core/common/src/main/kotlin/trebuchet/util/BufferReader.kt233
-rw-r--r--core/common/src/main/kotlin/trebuchet/util/Builders.kt52
-rw-r--r--core/common/src/main/kotlin/trebuchet/util/ByteArrayList.kt63
-rw-r--r--core/common/src/main/kotlin/trebuchet/util/PrintlnImportFeedback.kt30
-rw-r--r--core/common/src/main/kotlin/trebuchet/util/StringCache.kt34
-rw-r--r--core/common/src/main/kotlin/trebuchet/util/StringSearch.kt200
-rw-r--r--core/common/src/test/kotlin/trebuchet/extractors/SystraceExtractorTest.kt98
-rw-r--r--core/common/src/test/kotlin/trebuchet/extractors/ZlibExtractorTest.kt71
-rw-r--r--core/common/src/test/kotlin/trebuchet/importers/DummyImportFeedback.kt35
-rw-r--r--core/common/src/test/kotlin/trebuchet/importers/ftrace/EventTestHelpers.kt34
-rw-r--r--core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceEventTest.kt84
-rw-r--r--core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceImporterTest.kt176
-rw-r--r--core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceLineTest.kt136
-rw-r--r--core/common/src/test/kotlin/trebuchet/importers/ftrace/TracingMarkerEventTest.kt58
-rw-r--r--core/common/src/test/kotlin/trebuchet/io/StringStreamTest.kt78
-rw-r--r--core/common/src/test/kotlin/trebuchet/model/SliceGroupBuilderTest.kt69
-rw-r--r--core/common/src/test/kotlin/trebuchet/task/ImportTaskTest.kt83
-rw-r--r--core/common/src/test/kotlin/trebuchet/testutils/StringToStreamAdapter.kt34
-rw-r--r--core/model/build.gradle21
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/Constants.kt19
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/Counter.kt29
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/CpuModel.kt28
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/CpuProcessSlice.kt24
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/Model.kt65
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/ProcessModel.kt39
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/SchedSlice.kt23
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/SchedulingState.kt37
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/ThreadModel.kt31
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/base/Slice.kt42
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/base/SliceGroup.kt32
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/AutoCloseable.kt21
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/CounterFragment.kt23
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/CpuModelFragment.kt29
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/ModelFragment.kt35
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/ProcessModelFragment.kt80
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingProcessFragment.kt63
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingSliceFragment.kt46
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/SliceGroupBuilder.kt95
-rw-r--r--core/model/src/main/kotlin/trebuchet/model/fragments/ThreadModelFragment.kt45
-rw-r--r--gradle.properties2
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 54708 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew172
-rw-r--r--gradlew.bat84
-rw-r--r--scripts/README.md13
-rwxr-xr-xscripts/run-startup.sh41
-rw-r--r--settings.gradle29
-rw-r--r--trebuchet/analyzer/MANIFEST.mf2
-rw-r--r--trebuchet/analyzer/build.gradle33
-rw-r--r--trebuchet/analyzer/src/Analyzer.kt83
-rw-r--r--trebuchet/startup-analyzer/MANIFEST.mf2
-rw-r--r--trebuchet/startup-analyzer/README.md193
-rw-r--r--trebuchet/startup-analyzer/build.gradle33
-rw-r--r--trebuchet/startup-analyzer/src/StartupAnalyzer.kt319
-rw-r--r--trebuchet/traceutils/MANIFEST.mf2
-rw-r--r--trebuchet/traceutils/build.gradle33
-rw-r--r--trebuchet/traceutils/src/TraceUtils.kt107
-rw-r--r--trebuchet/viewer/MANIFEST.mf2
-rw-r--r--trebuchet/viewer/build.gradle28
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/Animator.kt56
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/LayoutConstants.kt23
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/Os.kt29
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessLabel.kt29
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessPanel.kt46
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/RenderState.kt58
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/RowLabel.kt33
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/ThemeColors.kt29
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/TimelineView.kt126
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/TraceViewerWindow.kt111
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/builders/Menu.kt64
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/components/ChooseTrace.kt45
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/MultiLineTrack.kt71
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SchedTrack.kt49
-rw-r--r--trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SliceTrack.kt106
-rw-r--r--trebuchet/viewer/src/main/kotlin/viewer.kt29
115 files changed, 7444 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ca35808
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+*/build
+*/*/build
+*/*/*/build
+*/*/*/*/build
+/out
+.externalNativeBuild
+.idea
+*/out
+*/*/out
+third_party/*/build
+third_party/*/out
+.project
+.settings/
+.classpath/
+*/.classpath
+*/*/.classpath
+*/*/bin
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..d963cb2
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,97 @@
+// Copyright (C) 2018 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.
+
+java_defaults {
+ name: "trebuchet-defaults",
+ javacflags: [
+ "-Xcoroutines=enable",
+ ],
+}
+
+java_library_host {
+ name: "trebuchet-core",
+ defaults: ["trebuchet-defaults"],
+ srcs: [
+ "core/common/src/main/**/*.kt",
+ "core/model/src/main/**/*.kt",
+ ],
+ libs: [
+ "kotlin-reflect",
+ ],
+ no_framework_libs: true,
+}
+
+java_test_host {
+ name: "trebuchet-core-tests",
+ defaults: ["trebuchet-defaults"],
+ srcs: [
+ "core/common/src/test/**/*.kt",
+ ],
+ static_libs: [
+ "trebuchet-core",
+ "kotlin-test",
+ ],
+ libs: [
+ "junit",
+ "kotlin-reflect",
+ ],
+}
+
+java_binary_host {
+ name: "AnalyzerKt",
+ defaults: ["trebuchet-defaults"],
+ manifest: "trebuchet/analyzer/MANIFEST.mf",
+ srcs: [
+ "trebuchet/analyzer/src/**/*.kt",
+ ],
+ static_libs: [
+ "trebuchet-core",
+ ],
+}
+
+java_binary_host {
+ name: "StartupAnalyzerKt",
+ defaults: ["trebuchet-defaults"],
+ manifest: "trebuchet/startup-analyzer/MANIFEST.mf",
+ srcs: [
+ "trebuchet/startup-analyzer/src/**/*.kt",
+ ],
+ static_libs: [
+ "trebuchet-core",
+ ],
+}
+
+java_binary_host {
+ name: "traceutils",
+ defaults: ["trebuchet-defaults"],
+ manifest: "trebuchet/traceutils/MANIFEST.mf",
+ srcs: [
+ "trebuchet/traceutils/src/**/*.kt",
+ ],
+ static_libs: [
+ "trebuchet-core",
+ ],
+}
+
+java_binary_host {
+ name: "traceviewer",
+ defaults: ["trebuchet-defaults"],
+ manifest: "trebuchet/viewer/MANIFEST.mf",
+ srcs: [
+ "trebuchet/viewer/src/main/**/*.kt",
+ ],
+ static_libs: [
+ "trebuchet-core",
+ ],
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6d364e1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests. \ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e1b4968
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+Trebuchet
+
+A library for parsing & analyzing atrace files.
+
+This is not an officially supported Google product.
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..4d79668
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "trebuchet-core-tests"
+ }
+ ]
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..d979e50
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+buildscript {
+ ext.kotlin_version = "1.2.51"
+ ext.rxjava2_version = "2.1.12"
+
+ repositories {
+ mavenCentral()
+ jcenter()
+ }
+
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ mavenCentral()
+ }
+} \ No newline at end of file
diff --git a/core/common/build.gradle b/core/common/build.gradle
new file mode 100644
index 0000000..768ef04
--- /dev/null
+++ b/core/common/build.gradle
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+apply plugin: 'kotlin-platform-jvm'
+
+repositories {
+ mavenCentral()
+ jcenter()
+}
+
+kotlin { experimental { coroutines 'enable' } }
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
+ implementation project(":core:model")
+ testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/collections/SparseArray.kt b/core/common/src/main/kotlin/trebuchet/collections/SparseArray.kt
new file mode 100644
index 0000000..d077a5a
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/collections/SparseArray.kt
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.collections
+
+@Suppress("UNCHECKED_CAST", "unused")
+class SparseArray<E> constructor(initialCapacity: Int = 10) {
+ private var mGarbage = false
+
+ private var mKeys: IntArray
+ private var mValues: Array<Any?>
+ private var mSize: Int = 0
+
+ init {
+ if (initialCapacity == 0) {
+ mKeys = EMPTY_INTS
+ mValues = EMPTY_OBJECTS
+ } else {
+ val idealSize = idealIntArraySize(initialCapacity)
+ mKeys = IntArray(idealSize)
+ mValues = arrayOfNulls<Any>(idealSize)
+ }
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or `null`
+ * if no such mapping has been made.
+ */
+ operator fun get(key: Int): E? {
+ return get(key, null)
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or the specified Object
+ * if no such mapping has been made.
+ */
+ operator fun get(key: Int, valueIfKeyNotFound: E?): E? {
+ val i = binarySearch(mKeys, mSize, key)
+
+ if (i < 0 || mValues[i] === DELETED) {
+ return valueIfKeyNotFound
+ } else {
+ return mValues[i] as E
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ fun delete(key: Int) {
+ val i = binarySearch(mKeys, mSize, key)
+
+ if (i >= 0) {
+ if (mValues[i] !== DELETED) {
+ mValues[i] = DELETED
+ mGarbage = true
+ }
+ }
+ }
+
+ /**
+ * Alias for [.delete].
+ */
+ fun remove(key: Int) {
+ delete(key)
+ }
+
+ /**
+ * Removes the mapping at the specified index.
+ */
+ fun removeAt(index: Int) {
+ if (mValues[index] !== DELETED) {
+ mValues[index] = DELETED
+ mGarbage = true
+ }
+ }
+
+ /**
+ * Remove a range of mappings as a batch.
+
+ * @param index Index to begin at
+ * *
+ * @param size Number of mappings to remove
+ */
+ fun removeAtRange(index: Int, size: Int) {
+ val end = minOf(mSize, index + size)
+ for (i in index..end - 1) {
+ removeAt(i)
+ }
+ }
+
+ private fun gc() {
+ val n = mSize
+ var o = 0
+ val keys = mKeys
+ val values = mValues
+
+ for (i in 0..n - 1) {
+ val `val` = values[i]
+
+ if (`val` !== DELETED) {
+ if (i != o) {
+ keys[o] = keys[i]
+ values[o] = `val`
+ values[i] = null
+ }
+
+ o++
+ }
+ }
+
+ mGarbage = false
+ mSize = o
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ fun put(key: Int, value: E) {
+ var i = binarySearch(mKeys, mSize, key)
+
+ if (i >= 0) {
+ mValues[i] = value
+ } else {
+ i = i.inv()
+
+ if (i < mSize && mValues[i] === DELETED) {
+ mKeys[i] = key
+ mValues[i] = value
+ return
+ }
+
+ if (mGarbage && mSize >= mKeys.size) {
+ gc()
+
+ // Search again because indices may have changed.
+ i = binarySearch(mKeys, mSize, key).inv()
+ }
+
+ if (mSize >= mKeys.size) {
+ val n = idealIntArraySize(mSize + 1)
+
+ val nkeys = IntArray(n)
+ val nvalues = arrayOfNulls<Any>(n)
+
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.size)
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.size)
+
+ mKeys = nkeys
+ mValues = nvalues
+ }
+
+ if (mSize - i != 0) {
+ System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i)
+ System.arraycopy(mValues, i, mValues, i + 1, mSize - i)
+ }
+
+ mKeys[i] = key
+ mValues[i] = value
+ mSize++
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseArray
+ * currently stores.
+ */
+ fun size(): Int {
+ if (mGarbage) {
+ gc()
+ }
+
+ return mSize
+ }
+
+ /**
+ * Given an index in the range `0...size()-1`, returns
+ * the key from the `index`th key-value mapping that this
+ * SparseArray stores.
+ */
+ fun keyAt(index: Int): Int {
+ if (mGarbage) {
+ gc()
+ }
+
+ return mKeys[index]
+ }
+
+ /**
+ * Given an index in the range `0...size()-1`, returns
+ * the value from the `index`th key-value mapping that this
+ * SparseArray stores.
+ */
+ fun valueAt(index: Int): E {
+ if (mGarbage) {
+ gc()
+ }
+
+ return mValues[index] as E
+ }
+
+ /**
+ * Given an index in the range `0...size()-1`, sets a new
+ * value for the `index`th key-value mapping that this
+ * SparseArray stores.
+ */
+ fun setValueAt(index: Int, value: E) {
+ if (mGarbage) {
+ gc()
+ }
+
+ mValues[index] = value
+ }
+
+ /**
+ * Returns the index for which [.keyAt] would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ fun indexOfKey(key: Int): Int {
+ if (mGarbage) {
+ gc()
+ }
+
+ return binarySearch(mKeys, mSize, key)
+ }
+
+ /**
+ * Returns an index for which [.valueAt] would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ *
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ *
+ * Note also that unlike most collections' `indexOf` methods,
+ * this method compares values using `==` rather than `equals`.
+ */
+ fun indexOfValue(value: E): Int {
+ if (mGarbage) {
+ gc()
+ }
+
+ for (i in 0..mSize - 1)
+ if (mValues[i] === value)
+ return i
+
+ return -1
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseArray.
+ */
+ fun clear() {
+ val n = mSize
+ val values = mValues
+
+ for (i in 0..n - 1) {
+ values[i] = null
+ }
+
+ mSize = 0
+ mGarbage = false
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ fun append(key: Int, value: E) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value)
+ return
+ }
+
+ if (mGarbage && mSize >= mKeys.size) {
+ gc()
+ }
+
+ val pos = mSize
+ if (pos >= mKeys.size) {
+ val n = idealIntArraySize(pos + 1)
+
+ val nkeys = IntArray(n)
+ val nvalues = arrayOfNulls<Any>(n)
+
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.size)
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.size)
+
+ mKeys = nkeys
+ mValues = nvalues
+ }
+
+ mKeys[pos] = key
+ mValues[pos] = value
+ mSize = pos + 1
+ }
+
+ /**
+ * {@inheritDoc}
+
+ *
+ * This implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ override fun toString(): String {
+ if (size() <= 0) {
+ return "{}"
+ }
+
+ val buffer = StringBuilder(mSize * 28)
+ buffer.append('{')
+ for (i in 0..mSize - 1) {
+ if (i > 0) {
+ buffer.append(", ")
+ }
+ val key = keyAt(i)
+ buffer.append(key)
+ buffer.append('=')
+ val value = valueAt(i)
+ if (value !== this) {
+ buffer.append(value)
+ } else {
+ buffer.append("(this Map)")
+ }
+ }
+ buffer.append('}')
+ return buffer.toString()
+ }
+
+ companion object {
+ private val DELETED = Any()
+
+ val EMPTY_INTS = IntArray(0)
+ val EMPTY_OBJECTS = arrayOfNulls<Any>(0)
+
+ fun idealIntArraySize(need: Int): Int {
+ return idealByteArraySize(need * 4) / 4
+ }
+
+ fun idealByteArraySize(need: Int): Int {
+ for (i in 4..31)
+ if (need <= (1 shl i) - 12)
+ return (1 shl i) - 12
+
+ return need
+ }
+
+ // This is Arrays.binarySearch(), but doesn't do any argument validation.
+ fun binarySearch(array: IntArray, size: Int, value: Int): Int {
+ var lo = 0
+ var hi = size - 1
+
+ while (lo <= hi) {
+ val mid = (lo + hi).ushr(1)
+ val midVal = array[mid]
+
+ if (midVal < value) {
+ lo = mid + 1
+ } else if (midVal > value) {
+ hi = mid - 1
+ } else {
+ return mid // value found
+ }
+ }
+ return lo.inv() // value not present
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/extractors/Extractor.kt b/core/common/src/main/kotlin/trebuchet/extractors/Extractor.kt
new file mode 100644
index 0000000..f49acb6
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/extractors/Extractor.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extractors
+
+import trebuchet.io.BufferProducer
+import trebuchet.io.StreamingReader
+
+/**
+ * An extractor that operates on string data sources
+ */
+interface Extractor {
+ /**
+ * Starts extraction from the given data source.
+ *
+ * @param stream The data to extract from
+ * @param processSubStream A callback mechanism to begin extracting sub-streams as encountered.
+ * The callback can be invoked as often as necessary. One extractor can produce
+ * multiple sub-streams.
+ */
+ fun extract(stream: StreamingReader, processSubStream: (BufferProducer) -> Unit)
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/extractors/ExtractorFactory.kt b/core/common/src/main/kotlin/trebuchet/extractors/ExtractorFactory.kt
new file mode 100644
index 0000000..c8aae97
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/extractors/ExtractorFactory.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extractors
+
+import trebuchet.importers.ImportFeedback
+import trebuchet.io.GenericByteBuffer
+
+interface ExtractorFactory {
+ fun extractorFor(buffer: GenericByteBuffer, feedback: ImportFeedback): Extractor?
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/extractors/ExtractorRegistry.kt b/core/common/src/main/kotlin/trebuchet/extractors/ExtractorRegistry.kt
new file mode 100644
index 0000000..1f39834
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/extractors/ExtractorRegistry.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extractors
+
+import trebuchet.importers.ImportFeedback
+import trebuchet.io.GenericByteBuffer
+
+object ExtractorRegistry {
+ private val extractors = arrayOf<ExtractorFactory>(
+ SystraceExtractor.Factory,
+ ZlibExtractor.Factory
+ )
+
+ fun extractorFor(buffer: GenericByteBuffer, feedback: ImportFeedback): Extractor? {
+ extractors.forEach {
+ val extractor = it.extractorFor(buffer, feedback)
+ if (extractor != null) return extractor
+ }
+ return null
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/extractors/SystraceExtractor.kt b/core/common/src/main/kotlin/trebuchet/extractors/SystraceExtractor.kt
new file mode 100644
index 0000000..1a7dcff
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/extractors/SystraceExtractor.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extractors
+
+import trebuchet.importers.ImportFeedback
+import trebuchet.io.*
+import trebuchet.util.contains
+import trebuchet.util.searchFor
+
+class SystraceExtractor : Extractor {
+ private var startIndex: Long = 0
+ private var endIndex: Long = Long.MAX_VALUE
+
+ override fun extract(stream: StreamingReader, processSubStream: (BufferProducer) -> Unit) {
+ while (true) {
+ stream.onWindowReleased = null
+ startIndex = START.find(stream, startIndex)
+ if (startIndex == -1L) return
+ startIndex += START.length
+ if (!stream.loadIndex(startIndex)) return
+ if (stream[startIndex] == '\n'.toByte()) startIndex++
+ endIndex = Long.MAX_VALUE
+
+ val pipe = Pipe<DataSlice>()
+ val thread = Thread {
+ stream.onWindowReleased = { window -> processWindow(window, pipe) }
+ endIndex = END.find(stream, startIndex)
+ if (endIndex == -1L) {
+ endIndex = stream.endIndex
+ }
+ stream.onWindowReleased = null
+ stream.windows.forEach { processWindow(it, pipe) }
+ pipe.close()
+ }
+ thread.start()
+
+ processSubStream(SubStream(pipe))
+
+ thread.join()
+
+ startIndex = endIndex
+ }
+ }
+
+ private class SubStream(val pipe: Pipe<DataSlice>) : BufferProducer {
+ override fun next(): DataSlice? = pipe.next()
+ }
+
+ private fun processWindow(window: StreamingReader.Window, pipe: Pipe<DataSlice>) {
+ if (window.globalEndIndex >= startIndex && window.globalStartIndex < endIndex) {
+ if (window.globalStartIndex >= startIndex && window.globalEndIndex <= endIndex) {
+ pipe.add(window.slice)
+ } else {
+ val sliceStart = maxOf(startIndex - window.globalStartIndex, 0)
+ val sliceEnd = minOf(endIndex, window.globalEndIndex) - window.globalStartIndex
+ pipe.add(window.slice.slice(sliceStart.toInt(), sliceEnd.toInt()))
+ }
+ }
+ }
+
+ private companion object {
+ val START = searchFor("""<script class="trace-data" type="application/text">""")
+ val END = searchFor("""</script>""")
+ }
+
+ object Factory : ExtractorFactory {
+ override fun extractorFor(buffer: GenericByteBuffer, feedback: ImportFeedback): Extractor? {
+ if (buffer.contains("<html>", 1000)) {
+ return SystraceExtractor()
+ }
+ return null
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/extractors/ZlibExtractor.kt b/core/common/src/main/kotlin/trebuchet/extractors/ZlibExtractor.kt
new file mode 100644
index 0000000..ed7b4dc
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/extractors/ZlibExtractor.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extractors
+
+import trebuchet.importers.ImportFeedback
+import trebuchet.io.*
+import trebuchet.util.indexOf
+import java.io.InputStream
+import java.util.zip.DataFormatException
+import java.util.zip.Inflater
+import java.util.zip.InflaterInputStream
+import kotlin.coroutines.experimental.buildIterator
+
+private const val TRACE = "TRACE:"
+
+private fun findStart(buffer: GenericByteBuffer): Long {
+ var start = buffer.indexOf(TRACE, 100)
+ if (start == -1L) {
+ start = 0L
+ } else {
+ start += TRACE.length
+ }
+ while (start < buffer.length &&
+ (buffer[start] == '\n'.toByte() || buffer[start] == '\r'.toByte())) {
+ start++
+ }
+ return start
+}
+
+private class DeflateProducer(stream: StreamingReader, val feedback: ImportFeedback)
+ : BufferProducer {
+
+ private val source = stream.source
+ private val inflater = Inflater()
+ private var closed = false
+
+ private val sourceIterator = buildIterator {
+ stream.loadIndex(stream.startIndex + 1024)
+ val offset = findStart(stream)
+ val buffIter = stream.iter(offset)
+ var avgCompressFactor = 5.0
+ while (buffIter.hasNext()) {
+ val nextBuffer = buffIter.next()
+ inflater.setInput(nextBuffer.buffer, nextBuffer.startIndex, nextBuffer.length)
+ do {
+ val remaining = inflater.remaining
+ val estSize = (remaining * avgCompressFactor * 1.2).toInt()
+ val array = ByteArray(estSize)
+ val len = inflater.inflate(array)
+ if (inflater.needsDictionary()) {
+ feedback.reportImportException(IllegalStateException(
+ "inflater needs dictionary, which isn't supported"))
+ return@buildIterator
+ }
+ val compressFactor = len.toDouble() / (remaining - inflater.remaining)
+ avgCompressFactor = (avgCompressFactor * 9 + compressFactor) / 10
+ yield(array.asSlice(len))
+ if (closed) return@buildIterator
+ } while (!inflater.needsInput())
+ inflater.end()
+ }
+ }
+
+ override fun next(): DataSlice? {
+ return if (sourceIterator.hasNext()) sourceIterator.next() else null
+ }
+
+ override fun close() {
+ closed = true
+ source.close()
+ inflater.end()
+ }
+}
+
+class ZlibExtractor(val feedback: ImportFeedback) : Extractor {
+
+ override fun extract(stream: StreamingReader, processSubStream: (BufferProducer) -> Unit) {
+ processSubStream(DeflateProducer(stream, feedback))
+ }
+
+ object Factory : ExtractorFactory {
+ private const val SIZE_TO_CHECK = 200
+
+ override fun extractorFor(buffer: GenericByteBuffer, feedback: ImportFeedback): Extractor? {
+ val start = findStart(buffer)
+ val toRead = minOf((buffer.length - start).toInt(), SIZE_TO_CHECK)
+ // deflate must contain at least a 2 byte header + 4 byte checksum
+ // So if there's less than 6 bytes this either isn't deflate or
+ // there's not enough data to try an inflate anyway
+ if (toRead <= 6) {
+ return null
+ }
+ val inflate = Inflater()
+ try {
+ val tmpBuffer = ByteArray(toRead) { buffer[start + it] }
+ inflate.setInput(tmpBuffer)
+ val result = ByteArray(1024)
+ val inflated = inflate.inflate(result)
+ inflate.end()
+ if (inflated > 0) {
+ return ZlibExtractor(feedback)
+ }
+ } catch (ex: DataFormatException) {
+ // Must not be deflate format
+ } finally {
+ inflate.end()
+ }
+ return null
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/extras/ImportUtils.kt b/core/common/src/main/kotlin/trebuchet/extras/ImportUtils.kt
new file mode 100644
index 0000000..6821093
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/extras/ImportUtils.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extras
+
+import trebuchet.model.Model
+import trebuchet.task.ImportTask
+import trebuchet.util.PrintlnImportFeedback
+import java.io.File
+
+fun Double.format(digits: Int) = String.format("%.${digits}f", this)
+
+fun reportImportProgress(read: Long, total: Long) {
+ if (total > 0) {
+ val progress = read.toDouble() / total.toDouble() * 100.0
+ print("\rProgress: ${progress.format(2)}%")
+ if (progress >= 100) { print("\n") }
+ }
+}
+
+fun parseTrace(file: File): Model {
+ val before = System.nanoTime()
+ val task = ImportTask(PrintlnImportFeedback())
+ val model = task.import(InputStreamAdapter(file, ::reportImportProgress))
+ val after = System.nanoTime()
+ val duration = (after - before) / 1000000
+ println("Parsing ${file.name} took ${duration}ms")
+ return model
+}
+
+fun findSampleData(): String {
+ var path = "sample_data"
+ while (!File(path).exists()) {
+ path = "../" + path
+ }
+ return path
+}
+
+fun openSample(name: String): Model {
+ return parseTrace(File(findSampleData(), name))
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/extras/StreamAdapter.kt b/core/common/src/main/kotlin/trebuchet/extras/StreamAdapter.kt
new file mode 100644
index 0000000..25f26c0
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/extras/StreamAdapter.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extras
+
+import trebuchet.io.BufferProducer
+import trebuchet.io.DataSlice
+import java.io.File
+import java.io.InputStream
+
+typealias ProgressCallback = ((Long, Long) -> Unit)
+
+class InputStreamAdapter(val inputStream: InputStream,
+ val totalSize: Long = 0,
+ val progressCallback: ProgressCallback? = null) : BufferProducer {
+ constructor(file: File, progressCallback: ProgressCallback? = null)
+ : this(file.inputStream(), file.length(), progressCallback)
+
+ var totalRead: Long = 0
+ var hitEof = false
+
+ override fun next(): DataSlice? {
+ if (hitEof) return null
+ val buffer = ByteArray(2 * 1024 * 1024)
+ val read = inputStream.read(buffer)
+ if (read == -1) {
+ hitEof = true
+ return null
+ }
+ totalRead += read
+ progressCallback?.let { it.invoke(totalRead, totalSize) }
+ return DataSlice(buffer, 0, read)
+ }
+
+ override fun close() {
+ hitEof = true
+ inputStream.close()
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ImportFeedback.kt b/core/common/src/main/kotlin/trebuchet/importers/ImportFeedback.kt
new file mode 100644
index 0000000..4ed9ff0
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ImportFeedback.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers
+
+interface ImportFeedback {
+ fun reportImportWarning(warning: String)
+ fun reportImportException(exception: Throwable)
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/Importer.kt b/core/common/src/main/kotlin/trebuchet/importers/Importer.kt
new file mode 100644
index 0000000..0fcbb2e
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/Importer.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers
+
+import trebuchet.io.StreamingReader
+import trebuchet.model.fragments.ModelFragment
+
+interface Importer {
+ /**
+ * Produces a ModelFragment from the given DataStream. The importer may return null at any point if it
+ * is unable to process the stream for whatever reason.
+ *
+ * @param stream The stream to read from
+ * @return A ModelFragment built from the input, or null if the importer was unable to import
+ */
+ fun import(stream: StreamingReader): ModelFragment?
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ImporterFactory.kt b/core/common/src/main/kotlin/trebuchet/importers/ImporterFactory.kt
new file mode 100644
index 0000000..337780e
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ImporterFactory.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers
+
+import trebuchet.io.GenericByteBuffer
+
+
+interface ImporterFactory {
+ fun importerFor(buffer: GenericByteBuffer, feedback: ImportFeedback): Importer?
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ImporterRegistry.kt b/core/common/src/main/kotlin/trebuchet/importers/ImporterRegistry.kt
new file mode 100644
index 0000000..d41405a
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ImporterRegistry.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers
+
+import trebuchet.importers.ftrace.FtraceImporter
+import trebuchet.io.GenericByteBuffer
+
+object ImporterRegistry {
+ private val importers = listOf<ImporterFactory>(
+ FtraceImporter.Factory
+ )
+
+ fun importerFor(buffer: GenericByteBuffer, feedback: ImportFeedback): Importer? {
+ importers.forEach {
+ val importer = it.importerFor(buffer, feedback)
+ if (importer != null) return importer
+ }
+ return null
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporter.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporter.kt
new file mode 100644
index 0000000..33838ca
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporter.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import trebuchet.importers.ImportFeedback
+import trebuchet.importers.Importer
+import trebuchet.importers.ImporterFactory
+import trebuchet.importers.ftrace.events.CpuBufferStarted
+import trebuchet.importers.ftrace.events.FtraceEvent
+import trebuchet.importers.ftrace.events.createEventParser
+import trebuchet.io.GenericByteBuffer
+import trebuchet.io.StreamingReader
+import trebuchet.io.iterLines
+import trebuchet.model.fragments.ModelFragment
+import trebuchet.util.contains
+import trebuchet.util.par_map
+
+class FtraceImporter(val feedback: ImportFeedback) : Importer {
+ var score: Long = 0
+ var state = FtraceImporterState()
+
+ override fun import(stream: StreamingReader): ModelFragment? {
+ par_map(stream.iterLines(), ::createEventParser) { parserState, it ->
+ try {
+ FtraceEvent.tryParseText(parserState, it)
+ } catch (t: Throwable) {
+ println("line '$it' failed")
+ t.printStackTrace()
+ null
+ }
+ }.forEach {
+ if (it == null) {
+ score--
+ if (score < -20) {
+ feedback.reportImportWarning("Too many errors, giving up")
+ return null
+ }
+ } else {
+ score++
+ if (it === CpuBufferStarted) {
+ state = FtraceImporterState()
+ } else {
+ state.importEvent(it)
+ }
+ }
+ }
+ return state.finish()
+ }
+
+ object Factory : ImporterFactory {
+ override fun importerFor(buffer: GenericByteBuffer, feedback: ImportFeedback): Importer? {
+ if (buffer.contains("# tracer: nop\n", 1000)
+ || buffer.contains(" trace-cmd", 1000)) {
+ return FtraceImporter(feedback)
+ }
+ return null
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporterState.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporterState.kt
new file mode 100644
index 0000000..b27729c
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceImporterState.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import trebuchet.collections.SparseArray
+import trebuchet.importers.ftrace.events.FtraceEvent
+import trebuchet.model.InvalidId
+import trebuchet.model.fragments.CpuModelFragment
+import trebuchet.model.fragments.ModelFragment
+import trebuchet.model.fragments.ProcessModelFragment
+import trebuchet.model.fragments.ThreadModelFragment
+
+class FtraceImporterState {
+ private val pidMap = SparseArray<ThreadModelFragment>(50)
+ private val cpuMap = SparseArray<CpuModelFragment>(6)
+ val modelFragment = ModelFragment()
+
+ fun finish(): ModelFragment {
+ return modelFragment
+ }
+
+ fun importEvent(event: FtraceEvent) {
+ if (modelFragment.globalStartTime == 0.0) {
+ modelFragment.globalStartTime = event.timestamp
+ }
+ modelFragment.globalEndTime = event.timestamp
+ event.import(this)
+ }
+
+ private fun createProcess(tgid: Int, name: String? = null): ThreadModelFragment {
+ val proc = ProcessModelFragment(tgid, name)
+ modelFragment.processes.add(proc)
+ val thread = proc.threadFor(tgid, name)
+ if (pidMap[tgid] != null) {
+ IllegalStateException("Unable to create process $tgid - already exists!")
+ }
+ pidMap.put(tgid, thread)
+ return thread
+ }
+
+ fun processFor(tgid: Int, name: String? = null): ProcessModelFragment {
+ val thread = pidMap[tgid] ?: createProcess(tgid, name)
+ thread.process.hint(tgid, name)
+ return thread.process
+ }
+
+ private fun createUnknownProcess(): ProcessModelFragment {
+ return ProcessModelFragment(InvalidId, hasIdCb = { process ->
+ val tgid = process.id
+ val existing = pidMap[tgid]
+ if (existing != null) {
+ existing.process.merge(process)
+ } else {
+ pidMap.put(tgid, process.threadFor(tgid, process.name))
+ }
+ if (modelFragment.processes.none { it.id == process.id }) {
+ modelFragment.processes.add(process)
+ }
+ })
+ }
+
+ fun threadFor(pid: Int, tgid: Int = InvalidId, task: String? = null): ThreadModelFragment {
+ var thread = pidMap[pid]
+ val processName = if (tgid == pid) task else null
+ if (thread != null) {
+ thread.hint(name = task, tgid = tgid, processName = processName)
+ return thread
+ }
+ val process =
+ if (tgid != InvalidId) processFor(tgid)
+ else createUnknownProcess()
+ thread = process.threadFor(pid, task)
+ thread.hint(processName = processName)
+ pidMap.put(pid, thread)
+ return thread
+ }
+
+ fun threadFor(line: FtraceLine) = threadFor(line.pid, line.tgid, line.task)
+ fun threadFor(event: FtraceEvent) = threadFor(event.pid, event.tgid, event.task)
+
+ fun cpuFor(cid: Int): CpuModelFragment {
+ var cpu = cpuMap[cid]
+ if (cpu == null) {
+ cpu = CpuModelFragment(cid)
+ modelFragment.cpus.add(cpu)
+ }
+ cpuMap.put(cid, cpu)
+ return cpuMap[cid]!!
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceLine.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceLine.kt
new file mode 100644
index 0000000..85049ab
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/FtraceLine.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import trebuchet.io.DataSlice
+import trebuchet.io.asSlice
+import trebuchet.model.InvalidId
+import trebuchet.util.BufferReader
+import trebuchet.util.StringCache
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import kotlin.concurrent.getOrSet
+
+@Suppress("unused")
+const val FtraceLineRE = """^*(.{1,16})-(\d+) +(?:\( *(\d+)?-*\) )?\[(\d+)] (?:[dX.]...)? *([\d.]*): *([^:]*): (.*)$"""
+
+class FtraceLine private constructor() {
+ private var _task: String? = null
+ private var _pid: Int = 0
+ private var _tgid: Int = 0
+ private var _cpu: Int = 0
+ private var _timestamp: Double = 0.0
+ private var _function: DataSlice = DataSlice()
+ private var _functionDetails: BufferReader? = null
+
+ val hasTgid: Boolean get() = _tgid != InvalidId
+ var tgid: Int
+ get() = _tgid
+ set(value) {
+ if (hasTgid && _tgid != value) {
+ throw IllegalStateException("tgid fight, currently $_tgid but trying to set $value")
+ }
+ _tgid = value
+ }
+ val task get() = _task
+ val pid get() = _pid
+ val cpu get() = _cpu
+ val timestamp get() = _timestamp
+ val function get() = _function
+ val functionDetailsReader get() = _functionDetails!!
+
+ private fun set(taskName: String?, pid: Int, tgid: Int, cpu: Int, timestamp: Double,
+ func: DataSlice, funcDetails: BufferReader) {
+ _task = taskName
+ _pid = pid
+ _tgid = tgid
+ _cpu = cpu
+ _timestamp = timestamp
+ _function = func
+ _functionDetails = funcDetails
+ }
+
+ companion object {
+ val INVALID_LINE = FtraceLine()
+
+ private val matcherLocal = ThreadLocal<Matcher>()
+ private val readerLocal = ThreadLocal<BufferReader>()
+
+ private val matcher: Matcher
+ get() = matcherLocal.getOrSet { Pattern.compile(FtraceLineRE).matcher("") }
+ private val reader: BufferReader
+ get() = readerLocal.getOrSet { BufferReader() }
+
+ fun create(line: DataSlice, stringCache: StringCache): FtraceLine? {
+ try {
+ reader.read(line, stringCache) {
+ tryMatch(matcher) {
+ val ftraceLine = FtraceLine()
+ ftraceLine.set(
+ string(1),
+ int(2),
+ if (matcher!!.start(3) == -1) InvalidId else int(3),
+ int(4),
+ double(5),
+ slice(6),
+ reader(7)
+ )
+ return@create ftraceLine
+ }
+ }
+ } catch (ex: Exception) {
+
+ }
+ return null
+ }
+ }
+
+ class Parser(val stringCache: StringCache) {
+ private val NullTaskName = stringCache.stringFor("<...>".asSlice())
+ private val ftraceLine = FtraceLine()
+ private val _reader = BufferReader()
+ private val matcher = Pattern.compile(FtraceLineRE).matcher("")
+
+ fun parseLine_new(line: DataSlice, callback: (FtraceLine) -> Unit) =
+ _reader.read(line, stringCache) {
+ match(matcher) {
+ ftraceLine.set(
+ string(1),
+ int(2),
+ if (matcher!!.start(3) == -1) InvalidId else int(3),
+ int(4),
+ double(5),
+ slice(6),
+ reader(7)
+ )
+ callback(ftraceLine)
+ }
+ }
+
+ fun parseLine(line: DataSlice, callback: (FtraceLine) -> Unit) =
+ _reader.read(line, stringCache) {
+ var tgid: Int = InvalidId
+ skipChar(' '.toByte())
+ val taskName = stringTo {
+ skipUntil { it == '-'.toByte() }
+ skipUntil { it == '('.toByte() || it == '['.toByte() }
+ rewindUntil { it == '-'.toByte() }
+ }
+ val pid = readInt()
+ skipChar(' '.toByte())
+ if (peek() == '('.toByte()) {
+ skip()
+ if (peek() != '-'.toByte()) {
+ tgid = readInt()
+ }
+ skipUntil { it == ')'.toByte() }
+ }
+ val cpu = readInt()
+ skipCount(2)
+ if (peek() == '.'.toByte() || peek() > '9'.toByte()) {
+ skipCount(5)
+ }
+ skipChar(' '.toByte())
+ val timestamp = readDouble()
+ skipCount(1); skipChar(' '.toByte())
+ val func = sliceTo(ftraceLine.function) { skipUntil { it == ':'.toByte() } }
+ skipCount(1)
+ skipChar(' '.toByte())
+ ftraceLine.set(if (taskName === NullTaskName) null else taskName, pid, tgid, cpu,
+ timestamp, func, _reader)
+ callback(ftraceLine)
+ }
+ }
+}
+
+fun FtraceLine.valid() = this !== FtraceLine.INVALID_LINE
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/ImportData.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/ImportData.kt
new file mode 100644
index 0000000..4324110
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/ImportData.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import trebuchet.importers.ImportFeedback
+import trebuchet.util.BufferReader
+
+data class ImportData(val importer: FtraceImporterState, val feedback: ImportFeedback) {
+ private var _line: FtraceLine? = null
+
+ fun wrap(line: FtraceLine): ImportData {
+ _line = line
+ return this
+ }
+
+ val line: FtraceLine get() = _line!!
+ val thread get() = importer.threadFor(line)
+
+ inline fun <T> readDetails(init: BufferReader.() -> T): T {
+ return line.functionDetailsReader.init()
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/EventParserState.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/EventParserState.kt
new file mode 100644
index 0000000..ee207e2
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/EventParserState.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace.events
+
+import trebuchet.importers.ftrace.FtraceImporterState
+import trebuchet.importers.ftrace.FtraceLine
+import trebuchet.io.DataSlice
+import trebuchet.model.InvalidId
+import trebuchet.util.BufferReader
+import trebuchet.util.MatchResult
+import trebuchet.util.StringCache
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+const val FtraceLineRE = """^ *(.{1,16})-(\d+) +(?:\( *(\d+)?-*\) )?\[(\d+)] (?:[dX.]...)? *([\d.]*): *([^:]*): *(.*) *$"""
+
+interface FtraceEventDetails {
+ fun import(event: FtraceEvent, state: FtraceImporterState)
+}
+
+val NoDetails = object : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {}
+}
+
+typealias EventDetailsParser = (state: EventParserState, slice: DataSlice) -> FtraceEventDetails?
+
+class SharedEventParserState {
+ private val patterns = mutableListOf<Pattern>()
+ private val detailHandlers = hashMapOf<String, EventDetailsParser>()
+ private var frozen = false
+
+ init {
+ EventRegistry.forEach { it(this) }
+ }
+
+ fun addPattern(newPattern: String) = addPattern(Pattern.compile(newPattern))
+
+ fun addPattern(newPattern: Pattern): Int {
+ if (frozen) throw IllegalStateException("State is frozen")
+ val index = patterns.size
+ patterns.add(newPattern)
+ return index
+ }
+
+ fun createParser(): EventParserState {
+ frozen = true
+ return EventParserState(patterns, detailHandlers)
+ }
+
+ fun onParseDetails(funcName: String, parser: EventDetailsParser) {
+ if (frozen) throw IllegalStateException("State is frozen")
+ detailHandlers[funcName] = parser
+ }
+
+ inline fun onParseDetailsWithMatch(funcName: String, regex: String,
+ crossinline result: MatchResult.() -> FtraceEventDetails?) {
+ val patternMatcher = addPattern(regex)
+ onParseDetails(funcName) { state, details ->
+ state.ifMatches(patternMatcher, details) {
+ return@onParseDetails this.result()
+ }
+ null
+ }
+ }
+
+ inline fun onParseDetailsWithMatch(funcNames: Array<String>, regex: String,
+ crossinline result: MatchResult.() -> FtraceEventDetails?) {
+ val patternMatcher = addPattern(regex)
+ funcNames.forEach { funcName ->
+ onParseDetails(funcName) { state, details ->
+ state.ifMatches(patternMatcher, details) {
+ return@onParseDetails this.result()
+ }
+ null
+ }
+ }
+ }
+}
+
+val GlobalSharedParserState = SharedEventParserState()
+
+fun createEventParser() = GlobalSharedParserState.createParser()
+
+class EventParserState constructor(patterns: List<Pattern>,
+ private val detailHandlers: Map<String, EventDetailsParser>) {
+ val matchers = Array<Matcher>(patterns.size) { patterns[it].matcher("") }
+ val stringCache = StringCache()
+ val reader = BufferReader()
+
+ inline fun ifMatches(matcher: Int, slice: DataSlice, result: MatchResult.() -> Unit): Boolean {
+ reader.reset(slice, stringCache)
+ return reader.tryMatch(matchers[matcher], result)
+ }
+
+ fun detailsForText(funcName: CharSequence, detailsSlice: DataSlice): FtraceEventDetails {
+ return detailHandlers[funcName]?.invoke(this, detailsSlice) ?: NoDetails
+ }
+
+ inline fun <T> readDetails(detailsSlice: DataSlice, init: BufferReader.() -> T): T {
+ return reader.read(detailsSlice, stringCache, init)
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEvent.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEvent.kt
new file mode 100644
index 0000000..83f87e7
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEvent.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace.events
+
+import trebuchet.importers.ftrace.FtraceImporterState
+import trebuchet.io.DataSlice
+import trebuchet.model.InvalidId
+
+val CpuBufferStarted = FtraceEvent(null, InvalidId, InvalidId, -1, -1.0,
+ "CPU BUFFER STARTED", NoDetails)
+
+class FtraceEvent(val task: String?,
+ val pid: Int,
+ val tgid: Int,
+ val cpu: Int,
+ val timestamp: Double,
+ val function: String,
+ val details: FtraceEventDetails) {
+
+ fun import(state: FtraceImporterState) {
+ details.import(this, state)
+ }
+
+ companion object {
+ private var ftraceLineMatcher: Int = -1
+ private var cpuBufferStarted: Int = -1
+
+ val register: EventRegistryEntry = { sharedState ->
+ ftraceLineMatcher = sharedState.addPattern(FtraceLineRE)
+ cpuBufferStarted = sharedState.addPattern("##### CPU \\d+ buffer started ####")
+ }
+
+ fun tryParseText(state: EventParserState, slice: DataSlice): FtraceEvent? {
+ state.ifMatches(ftraceLineMatcher, slice) {
+ val task = string(1)
+ val function = string(6)
+ return FtraceEvent(
+ task = if (task == "<...>") null else task,
+ pid = int(2),
+ tgid = intOr(3, InvalidId),
+ cpu = int(4),
+ timestamp = double(5),
+ function = function,
+ details = state.detailsForText(function, slice(7))
+ )
+ }
+ state.ifMatches(cpuBufferStarted, slice) {
+ return CpuBufferStarted
+ }
+ return null
+ }
+
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEventRegistry.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEventRegistry.kt
new file mode 100644
index 0000000..26bd14b
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/FtraceEventRegistry.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace.events
+
+typealias EventRegistryEntry = (SharedEventParserState) -> Unit
+val EventRegistry = arrayOf(
+ FtraceEvent.register,
+ TraceMarkerEvent.register,
+ SchedEvent.register,
+ WorkqueueEvent.register
+) \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/SchedEvent.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/SchedEvent.kt
new file mode 100644
index 0000000..d8c0f97
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/SchedEvent.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace.events
+
+import trebuchet.importers.ftrace.FtraceImporterState
+import trebuchet.model.SchedulingState
+import trebuchet.util.MatchResult
+import trebuchet.util.PreviewReader
+
+private fun PreviewReader.readSchedulingState(): SchedulingState {
+ val byte = readByte()
+ return when (byte) {
+ 'S'.toByte() -> SchedulingState.SLEEPING
+ 'R'.toByte() -> SchedulingState.RUNNABLE
+ 'D'.toByte() -> {
+ if (peek() == '|'.toByte()) {
+ skip()
+ return when (readByte()) {
+ 'K'.toByte() -> SchedulingState.UNINTR_SLEEP_WAKE_KILL
+ 'W'.toByte() -> SchedulingState.UNINTR_SLEEP_WAKING
+ else -> SchedulingState.UNINTR_SLEEP
+ }
+ }
+ SchedulingState.UNINTR_SLEEP
+ }
+ 'T'.toByte() -> SchedulingState.STOPPED
+ 't'.toByte() -> SchedulingState.DEBUG
+ 'Z'.toByte() -> SchedulingState.ZOMBIE
+ 'X'.toByte() -> SchedulingState.EXIT_DEAD
+ 'x'.toByte() -> SchedulingState.TASK_DEAD
+ 'K'.toByte() -> SchedulingState.WAKE_KILL
+ 'W'.toByte() -> SchedulingState.WAKING
+ else -> SchedulingState.UNKNOWN
+ }
+}
+
+private fun MatchResult.schedState(group: Int) = read(group) { readSchedulingState() }
+
+class SchedSwitchEvent(val prevComm: String, val prevPid: Int, val prevPrio: Int,
+ val prevState: SchedulingState, val nextComm: String, val nextPid: Int,
+ val nextPrio: Int) : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ val prevThread = state.threadFor(prevPid, task = prevComm)
+ val nextThread = state.threadFor(nextPid, task = nextComm)
+ val cpu = state.cpuFor(event.cpu)
+
+ prevThread.schedulingStateBuilder.switchState(prevState, event.timestamp)
+ nextThread.schedulingStateBuilder.switchState(SchedulingState.RUNNING, event.timestamp)
+ cpu.schedulingProcessBuilder.switchProcess(nextThread.process, nextThread, event.timestamp)
+ }
+}
+
+class SchedWakeupEvent(val comm: String, val pid: Int, val prio: Int, val targetCpu: Int)
+ : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ val thread = state.threadFor(pid, task = comm)
+ thread.schedulingStateBuilder.switchState(SchedulingState.WAKING, event.timestamp)
+ }
+}
+
+object SchedEvent {
+ private const val SchedSwitchRE = "prev_comm=(.*) prev_pid=(\\d+) prev_prio=(\\d+) prev_state=([^\\s]+) ==> next_comm=(.*) next_pid=(\\d+) next_prio=(\\d+)"
+ private const val SchedWakeupRE = """comm=(.+) pid=(\d+) prio=(\d+)(?: success=\d+)? target_cpu=(\d+)"""
+
+ val register: EventRegistryEntry = { sharedState ->
+ sharedState.onParseDetailsWithMatch("sched_switch", SchedSwitchRE) {
+ SchedSwitchEvent(string(1), int(2), int(3), schedState(4),
+ string(5), int(6), int(7))
+ }
+ sharedState.onParseDetailsWithMatch(arrayOf("sched_waking", "sched_wake"), SchedWakeupRE) {
+ SchedWakeupEvent(string(1), int(2), int(3), int(4))
+ }
+ }
+}
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/TraceMarkerEvent.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/TraceMarkerEvent.kt
new file mode 100644
index 0000000..176c9db
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/TraceMarkerEvent.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace.events
+
+import trebuchet.importers.ftrace.FtraceImporterState
+import trebuchet.util.BufferReader
+
+class BeginSliceEvent(val tgid: Int, val title: String) : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ state.threadFor(event.pid, tgid, event.task).slicesBuilder.beginSlice {
+ it.startTime = event.timestamp
+ it.name = title
+ }
+ }
+}
+
+class EndSliceEvent : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ state.threadFor(event.pid, event.tgid, event.task).slicesBuilder.endSlice {
+ it.endTime = event.timestamp
+ }
+ }
+}
+
+class CounterSliceEvent(val tgid: Int, val name: String, val value: Int) : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ state.threadFor(event.pid, tgid, event.task).process
+ .addCounterSample(name, event.timestamp, value)
+ }
+
+}
+
+private fun BufferReader.readBeginSlice(): BeginSliceEvent {
+ // Begin format: B|<tgid>|<title>
+ skipCount(2)
+ val tgid = readInt()
+ skip()
+ val name = stringTo { end() }
+ return BeginSliceEvent(tgid, name)
+}
+
+private fun BufferReader.readCounter(): CounterSliceEvent {
+ // Counter format: C|<tgid>|<name>|<value>
+ skipCount(2)
+ val tgid = readInt()
+ skip()
+ val name = stringTo { skipUntil { it == '|'.toByte() } }
+ skip()
+ val value = readInt()
+ return CounterSliceEvent(tgid, name, value)
+}
+
+class ParentTSEvent(val parentTimestamp: Double) : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ state.modelFragment.parentTimestamp = parentTimestamp
+ }
+}
+
+class RealtimeTSEvent(val realtimeTimestamp: Long) : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ state.modelFragment.realtimeTimestamp = realtimeTimestamp
+ }
+}
+
+object TraceMarkerEvent {
+ const val Begin = 'B'.toByte()
+ const val End = 'E'.toByte()
+ const val Counter = 'C'.toByte()
+
+ val register: EventRegistryEntry = { sharedState ->
+
+ val parentTsMatcher = sharedState.addPattern(
+ "trace_event_clock_sync: parent_ts=(.*)")
+ val realtimeTsMatcher = sharedState.addPattern(
+ "trace_event_clock_sync: realtime_ts=(.*)")
+
+ fun BufferReader.readClockSyncMarker(state: EventParserState): FtraceEventDetails? {
+ // First check if the line we are importing is the parent timestamp line.
+ tryMatch(state.matchers[parentTsMatcher]) {
+ return ParentTSEvent(double(1))
+ }
+ // Test if the line we are testing has the realtime timestamp.
+ tryMatch(state.matchers[realtimeTsMatcher]) {
+ return RealtimeTSEvent(long(1))
+ }
+ return null
+ }
+
+ sharedState.onParseDetails("tracing_mark_write") { state, details ->
+ state.readDetails(details) {
+ when (peek()) {
+ Begin -> readBeginSlice()
+ End -> EndSliceEvent()
+ Counter -> readCounter()
+ else -> readClockSyncMarker(state)
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/WorkqueueEvent.kt b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/WorkqueueEvent.kt
new file mode 100644
index 0000000..2381ab4
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/importers/ftrace/events/WorkqueueEvent.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace.events
+
+import trebuchet.importers.ftrace.FtraceImporterState
+
+class WorkqueueExecuteStartEvent(val struct: String, val function: String) : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ state.threadFor(event).slicesBuilder.beginSlice {
+ it.name = function
+ it.startTime = event.timestamp
+ }
+ }
+}
+
+class WorkqueueExecuteEndEvent(val struct: String) : FtraceEventDetails {
+ override fun import(event: FtraceEvent, state: FtraceImporterState) {
+ state.threadFor(event).slicesBuilder.endSlice {
+ it.endTime = event.timestamp
+ }
+ }
+}
+
+object WorkqueueEvent {
+ private const val workqueueExecuteStartRE = """work struct (\w+): function (\S+)"""
+ private const val workqueueExecuteEndRE = """work struct (\w+)"""
+
+ val register: EventRegistryEntry = { sharedState ->
+ sharedState.onParseDetailsWithMatch("workqueue_execute_start", workqueueExecuteStartRE) {
+ WorkqueueExecuteStartEvent(string(1), string(2))
+ }
+ sharedState.onParseDetailsWithMatch("workqueue_execute_end", workqueueExecuteEndRE) {
+ WorkqueueExecuteEndEvent(string(1))
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/io/BufferProducer.kt b/core/common/src/main/kotlin/trebuchet/io/BufferProducer.kt
new file mode 100644
index 0000000..e62a40f
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/io/BufferProducer.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.io
+
+interface BufferProducer {
+ fun next(): DataSlice?
+
+ fun close() {
+ while (next() != null) {}
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/io/DataSlice.kt b/core/common/src/main/kotlin/trebuchet/io/DataSlice.kt
new file mode 100644
index 0000000..b2578a7
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/io/DataSlice.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.io
+
+data class DataSlice(var buffer: ByteArray,
+ var startIndex: Int,
+ var endIndex: Int) {
+
+ companion object {
+ val EmptyBuffer = ByteArray(0)
+ }
+
+ constructor() : this(EmptyBuffer, 0, 0)
+ constructor(buffer: ByteArray) : this(buffer, 0, buffer.size)
+
+ @Suppress("NOTHING_TO_INLINE")
+ inline operator fun get(i: Int): Byte = buffer[startIndex + i]
+
+ @Suppress("NOTHING_TO_INLINE")
+ inline fun slice(startIndex: Int, endIndex: Int = this.endIndex,
+ dest: DataSlice = DataSlice()): DataSlice {
+ dest.set(buffer, this.startIndex + startIndex, this.startIndex + endIndex)
+ return dest
+ }
+
+ inline val length: Int get() = endIndex - startIndex
+
+ fun set(buffer: ByteArray, startIndex: Int, endIndex: Int) {
+ this.buffer = buffer
+ this.startIndex = startIndex
+ this.endIndex = endIndex
+ _hasCachedHashCode = false
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ if (other !is DataSlice) return false
+ if (length != other.length) return false
+ if (_hasCachedHashCode && other._hasCachedHashCode
+ && hashCode() != other.hashCode()) return false
+ var myIndex = startIndex
+ var otherIndex = other.startIndex
+ while (myIndex < endIndex) {
+ if (buffer[myIndex] != other.buffer[otherIndex]) {
+ return false
+ }
+ myIndex++
+ otherIndex++
+ }
+ return true
+ }
+
+ private var _cachedHashCode: Int = 1
+ private var _hasCachedHashCode: Boolean = false
+ override fun hashCode(): Int {
+ if (_hasCachedHashCode) return _cachedHashCode
+ var hash = 1
+ for (i in startIndex..endIndex-1) {
+ hash = 31 * hash + buffer[i].toInt()
+ }
+ _cachedHashCode = hash
+ _hasCachedHashCode = true
+ return hash
+ }
+
+ override fun toString() = String(buffer, startIndex, length)
+
+ fun compact(): DataSlice {
+ if (length - buffer.size < 50) {
+ return this
+ }
+ return DataSlice(buffer.copyOfRange(startIndex, endIndex))
+ }
+}
+
+fun ByteArray.asSlice(length: Int = this.size) = DataSlice(this, 0, length)
+fun String.asSlice() = DataSlice(this.toByteArray())
+
+fun DataSlice.asText(): CharSequence {
+ val data = this
+ return object : CharSequence {
+ override val length: Int get() = data.length
+
+ override fun get(index: Int): Char {
+ return data[index].toChar()
+ }
+
+ override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
+ return data.slice(startIndex, endIndex).asText()
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/io/GenericByteBuffer.kt b/core/common/src/main/kotlin/trebuchet/io/GenericByteBuffer.kt
new file mode 100644
index 0000000..bb99ee4
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/io/GenericByteBuffer.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.io
+
+interface GenericByteBuffer {
+ val length: Long
+ operator fun get(index: Long): Byte
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/io/Pipe.kt b/core/common/src/main/kotlin/trebuchet/io/Pipe.kt
new file mode 100644
index 0000000..bf2b36f
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/io/Pipe.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.io
+
+import java.util.concurrent.ArrayBlockingQueue
+
+interface Producer<in T> {
+ fun add(data: T)
+ fun close()
+}
+
+interface Consumer<out T> {
+ fun next(): T?
+}
+
+class Pipe<T>(capacity: Int = 4) : Producer<T>, Consumer<T> {
+ private class Packet<out T>(val data: T?)
+
+ private val queue = ArrayBlockingQueue<Packet<T>>(capacity)
+ private var producerClosed = false
+ private var consumerClosed = false
+
+ override fun add(data: T) {
+ if (data == null) throw IllegalStateException("Unable to send null")
+
+ if (producerClosed) throw IllegalStateException("Already closed")
+ queue.put(Packet(data))
+ }
+
+ override fun close() {
+ if (!producerClosed) {
+ producerClosed = true
+ queue.put(Packet(null))
+ }
+ }
+
+ override fun next(): T? {
+ if (consumerClosed) return null
+ val packet = queue.take()
+ if (packet.data == null) {
+ consumerClosed = true
+ }
+ return packet.data
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/io/StreamingLineReader.kt b/core/common/src/main/kotlin/trebuchet/io/StreamingLineReader.kt
new file mode 100644
index 0000000..cc07a0b
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/io/StreamingLineReader.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.io
+
+import kotlin.coroutines.experimental.buildIterator
+
+private fun findNewlineInWindow(window: StreamingReader.Window, startIndex: Long): Long {
+ for (i in startIndex..window.globalEndIndex) {
+ if (window[i] == '\n'.toByte()) return i
+ }
+ return -1
+}
+
+/**
+ * Iterates over all the lines in the stream, skipping any empty line. Lines do not contain
+ * the line-end marker(s). Handles both LF & CRLF endings.
+ */
+fun StreamingReader.iterLines() = buildIterator {
+ val stream = this@iterLines
+
+ var lineStartIndex = stream.startIndex
+ while (true) {
+ var index = lineStartIndex
+ var foundAt = -1L
+ while (true) {
+ if (index > stream.endIndex) {
+ if (!stream.loadIndex(index)) break
+ }
+ val window = stream.windowFor(index)
+ foundAt = findNewlineInWindow(window, index)
+ if (foundAt != -1L) break
+ index = window.globalEndIndex + 1
+ }
+ // Reached EOF with no data, return
+ if (lineStartIndex > stream.endIndex) break
+
+ // Reached EOF, consume remaining as a line
+ if (foundAt == -1L) foundAt = stream.endIndex + 1
+
+ val nextStart = foundAt + 1
+
+ // Handle CLRF
+ if (foundAt > 0 && stream[foundAt - 1] == '\r'.toByte()) foundAt -= 1
+
+ val lineEndIndexInclusive = foundAt - 1
+ if (lineStartIndex >= stream.startIndex && (lineEndIndexInclusive - lineStartIndex) >= 0) {
+ val window = stream.windowFor(lineStartIndex)
+ if (window === stream.windowFor(lineEndIndexInclusive)) {
+ // slice endIndex is exclusive
+ yield(window.slice.slice((lineStartIndex - window.globalStartIndex).toInt(),
+ (lineEndIndexInclusive - window.globalStartIndex + 1).toInt()))
+ } else {
+ var tmpBuffer = ByteArray((lineEndIndexInclusive - lineStartIndex + 1).toInt())
+ stream.copyTo(tmpBuffer, lineStartIndex, lineEndIndexInclusive)
+ yield(tmpBuffer.asSlice())
+ }
+ }
+ lineStartIndex = nextStart
+ }
+}
diff --git a/core/common/src/main/kotlin/trebuchet/io/StreamingReader.kt b/core/common/src/main/kotlin/trebuchet/io/StreamingReader.kt
new file mode 100644
index 0000000..18c87b6
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/io/StreamingReader.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.io
+
+import javax.xml.crypto.Data
+import kotlin.coroutines.experimental.buildIterator
+
+class StreamingReader(val source: BufferProducer, val keepLoadedSize: Int = 8096) : GenericByteBuffer {
+ val windows = mutableListOf<Window>()
+
+ var onWindowReleased: ((Window) -> Unit)? = null
+ var startIndex: Long = 0
+ get private set
+ var endIndex: Long = -1
+ get private set
+ var reachedEof: Boolean = false
+ get private set
+
+ override operator fun get(index: Long): Byte = windowFor(index)[index]
+ override val length: Long
+ get() = endIndex - startIndex + 1
+
+ fun windowFor(i: Long): Window {
+ for (wi in 0..windows.size-1) {
+ val window = windows[wi]
+ if (window.globalStartIndex <= i && window.globalEndIndex >= i) {
+ return window
+ }
+ }
+ throw IndexOutOfBoundsException("$i not in range $startIndex..$endIndex")
+ }
+
+ fun loadIndex(index: Long): Boolean {
+ while (endIndex < index && !reachedEof) {
+ val nextBuffer = source.next()
+ if (nextBuffer == null) {
+ reachedEof = true
+ return false
+ }
+ addBuffer(nextBuffer)
+ }
+ return index <= endIndex
+ }
+
+ fun iter(startIndex: Long = 0L): Iterator<DataSlice> {
+ return buildIterator {
+ for (win in windows) {
+ if (startIndex <= win.globalStartIndex) {
+ yield(win.slice)
+ } else if (startIndex <= win.globalEndIndex) {
+ yield(win.slice.slice((startIndex - win.globalStartIndex).toInt()))
+ }
+ }
+ while (!reachedEof) {
+ val nextBuffer = source.next()
+ if (nextBuffer == null) {
+ reachedEof = true
+ break
+ }
+ addBuffer(nextBuffer)
+ yield(nextBuffer!!)
+ }
+ }
+ }
+
+ private fun addBuffer(buffer: DataSlice) {
+ windows.add(Window(buffer, endIndex + 1, endIndex + buffer.length))
+ endIndex += buffer.length
+ if (windows.size > 2 && endIndex - windows[1].globalStartIndex > keepLoadedSize) {
+ val temp = windows[0]
+ windows.removeAt(0)
+ startIndex = windows[0].globalStartIndex
+ if (onWindowReleased != null) {
+ onWindowReleased!!.invoke(temp)
+ }
+ }
+ }
+
+ class Window(val slice: DataSlice, val globalStartIndex: Long, val globalEndIndex: Long) {
+ @Suppress("NOTHING_TO_INLINE")
+ inline operator fun get(i: Long): Byte = slice[(i - globalStartIndex).toInt()]
+ }
+
+ fun copyTo(tmpBuffer: ByteArray, lineStartIndex: Long, lineEndIndex: Long) {
+ var srcIndex = lineStartIndex
+ var dstIndex = 0
+ while (srcIndex <= lineEndIndex && dstIndex < tmpBuffer.size) {
+ val window = windowFor(srcIndex)
+ while (srcIndex <= window.globalEndIndex && dstIndex < tmpBuffer.size) {
+ tmpBuffer[dstIndex++] = window[srcIndex++]
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/queries/SliceQueries.kt b/core/common/src/main/kotlin/trebuchet/queries/SliceQueries.kt
new file mode 100644
index 0000000..c169889
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/queries/SliceQueries.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.queries
+
+import trebuchet.model.Model
+import trebuchet.model.ProcessModel
+import trebuchet.model.ThreadModel
+import trebuchet.model.base.Slice
+import trebuchet.model.base.SliceGroup
+
+enum class TraverseAction {
+ /**
+ * Continue iterating over any child slices
+ */
+ VISIT_CHILDREN,
+ /**
+ * There is no need to process any child slices
+ */
+ DONE,
+}
+
+interface SliceTraverser {
+ fun beginSlice(slice: Slice): TraverseAction = TraverseAction.VISIT_CHILDREN
+ fun endSlice(slice: Slice) {}
+}
+
+/**
+ * SliceQueries provides several methods of operating over all the slices in a model in bulk.
+ *
+ * [selectAll] finds all slices that match the given predicate.
+ *
+ * [iterSlices] applies a function to all slices in a model.
+ *
+ * [traverseSlices] is a more powerful version of [iterSlices]. It takes a [SliceTraverser], which
+ * allows code to be run at the beginning and end of processing a slice. This allows the
+ * [SliceTraverser] to, for example, keep track of how deep it is within the tree.
+ * The [SliceTraverser] can also indicate whether [traverseSlices] should continue processing
+ * child slices in the case of a [SliceGroup].
+ */
+object SliceQueries {
+ fun selectAll(model: Model, cb: (Slice) -> Boolean): List<Slice> {
+ val ret = mutableListOf<Slice>()
+ iterSlices(model) {
+ if (cb(it)) {
+ ret.add(it)
+ }
+ }
+ return ret
+ }
+
+ fun selectAll(thread: ThreadModel, cb: (Slice) -> Boolean): List<Slice> {
+ val ret = mutableListOf<Slice>()
+ iterSlices(thread) {
+ if (cb(it)) {
+ ret.add(it)
+ }
+ }
+ return ret
+ }
+
+ fun traverseSlices(model: Model, cb: SliceTraverser) {
+ model.processes.values.forEach { traverseSlices(it, cb) }
+ }
+
+ fun traverseSlices(process: ProcessModel, cb: SliceTraverser) {
+ process.threads.forEach { traverseSlices(it, cb) }
+ }
+
+ fun traverseSlices(thread: ThreadModel, cb: SliceTraverser) {
+ traverseSlices(thread.slices, cb)
+ }
+
+ fun traverseSlices(slices: List<SliceGroup>, cb: SliceTraverser) {
+ slices.forEach {
+ val action = cb.beginSlice(it)
+ if (action == TraverseAction.VISIT_CHILDREN)
+ traverseSlices(it.children, cb)
+ cb.endSlice(it)
+ }
+ }
+
+ fun iterSlices(model: Model, cb: (Slice) -> Unit) {
+ model.processes.values.forEach { iterSlices(it, cb) }
+ }
+
+ fun iterSlices(process: ProcessModel, cb: (Slice) -> Unit) {
+ process.threads.forEach { iterSlices(it, cb) }
+ }
+
+ fun iterSlices(thread: ThreadModel, cb: (Slice) -> Unit) {
+ iterSlices(thread.slices, cb)
+ }
+
+ fun iterSlices(slices: List<SliceGroup>, cb: (Slice) -> Unit) {
+ slices.forEach {
+ cb(it)
+ iterSlices(it.children, cb)
+ }
+ }
+
+ fun any(slices: List<SliceGroup>, cb: (Slice) -> Boolean): Boolean {
+ slices.forEach {
+ if (cb(it)) return true
+ if (any(it.children, cb)) return true
+ }
+ return false
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/queries/ThreadQueries.kt b/core/common/src/main/kotlin/trebuchet/queries/ThreadQueries.kt
new file mode 100644
index 0000000..02bd8f2
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/queries/ThreadQueries.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.queries
+
+import trebuchet.model.Model
+import trebuchet.model.ThreadModel
+
+
+object ThreadQueries {
+ fun firstOrNull(model: Model, predicate: (ThreadModel) -> Boolean): ThreadModel? {
+ model.processes.values.forEach {
+ it.threads.forEach {
+ if (predicate(it)) return it
+ }
+ }
+ return null
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/task/ImportTask.kt b/core/common/src/main/kotlin/trebuchet/task/ImportTask.kt
new file mode 100644
index 0000000..096aa07
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/task/ImportTask.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.task
+
+import trebuchet.extractors.ExtractorRegistry
+import trebuchet.importers.ImportFeedback
+import trebuchet.importers.ImporterRegistry
+import trebuchet.io.BufferProducer
+import trebuchet.io.StreamingReader
+import trebuchet.model.Model
+import trebuchet.model.fragments.ModelFragment
+import kotlin.system.measureTimeMillis
+
+class ImportTask(private val importFeedback: ImportFeedback) {
+ private val fragments = mutableListOf<ModelFragment>()
+
+ fun import(source: BufferProducer): Model {
+ var model: Model? = null
+ val duration = measureTimeMillis {
+ extractOrImport(source)
+ model = finish()
+ }
+ println("Took ${duration}ms to import")
+ return model!!
+ }
+
+ private fun extractOrImport(stream: BufferProducer) {
+ try {
+ val reader = StreamingReader(stream)
+ reader.loadIndex(reader.keepLoadedSize.toLong())
+ val extractor = ExtractorRegistry.extractorFor(reader, importFeedback)
+ if (extractor != null) {
+ extractor.extract(reader, this::extractOrImport)
+ } else {
+ addImporterSource(reader)
+ }
+ } catch (ex: Throwable) {
+ importFeedback.reportImportException(ex)
+ } finally {
+ stream.close()
+ }
+ }
+
+ private fun addImporterSource(reader: StreamingReader) {
+ val importer = ImporterRegistry.importerFor(reader, importFeedback)
+ if (importer != null) {
+ val result = importer.import(reader)
+ if (result != null) {
+ fragments.add(result)
+ }
+ }
+ }
+
+ private fun finish(): Model {
+ val model = Model(fragments)
+ fragments.clear()
+ return model
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/util/BatchProcessor.kt b/core/common/src/main/kotlin/trebuchet/util/BatchProcessor.kt
new file mode 100644
index 0000000..69e7362
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/util/BatchProcessor.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.util
+
+import java.io.Closeable
+import java.util.*
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.Callable
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicLong
+import kotlin.collections.ArrayList
+import kotlin.concurrent.thread
+import kotlin.system.measureNanoTime
+import kotlin.system.measureTimeMillis
+
+fun ns2ms(ns: Long) = ns / 1000000
+
+class WorkQueue {
+ private val workArray = ArrayDeque<Runnable?>(8192)
+ private val _finished = AtomicBoolean(false)
+
+ private val finished get() = _finished.get()
+ fun finish() = _finished.set(true)
+
+ private fun spinLoop(duration: Long) {
+ val loopUntil = System.nanoTime() + duration
+ while (System.nanoTime() < loopUntil) {}
+ }
+
+ fun submit(task: Runnable) {
+ synchronized(workArray) {
+ workArray.add(task)
+ }
+ }
+
+ fun processAll() {
+ while (!finished) {
+ var next: Runnable? = null
+ do {
+ synchronized(workArray) {
+ next = workArray.poll()
+ }
+ if (next == null) {
+ spinLoop(10000)
+ synchronized(workArray) {
+ next = workArray.poll()
+ }
+ }
+ next?.run()
+ } while (next != null)
+ spinLoop(60000)
+ }
+
+ var remaining: Runnable? = null
+ do {
+ synchronized(workArray) {
+ remaining = workArray.poll()
+ }
+ remaining?.run()
+ } while (remaining != null)
+ }
+}
+
+private val ThreadCount = minOf(10, Runtime.getRuntime().availableProcessors())
+
+class WorkPool : Closeable {
+
+ private var closed = false
+ private val workQueue = WorkQueue()
+ private val threadPool = Array(ThreadCount) {
+ Thread { workQueue.processAll() }.apply { start() }
+ }
+
+ fun submit(task: Runnable) {
+ workQueue.submit(task)
+ }
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ workQueue.finish()
+ threadPool.forEach { it.join() }
+ }
+}
+
+fun<T, U, S> par_map(iterator: Iterator<T>, threadState: () -> S, chunkSize: Int = 50,
+ map: (S, T) -> U): Iterator<U> {
+ val endOfStreamMarker = CompletableFuture<List<U>>()
+ val resultPipe = ArrayBlockingQueue<Future<List<U>>>(1024, false)
+ thread {
+ val threadLocal = ThreadLocal.withInitial { threadState() }
+ if (true) {
+ WorkPool().use { pool ->
+ while (iterator.hasNext()) {
+ val source = ArrayList<T>(chunkSize)
+ while (source.size < chunkSize && iterator.hasNext()) {
+ source.add(iterator.next())
+ }
+ val future = CompletableFuture<List<U>>()
+ pool.submit(Runnable {
+ val state = threadLocal.get()
+ future.complete(source.map { map(state, it) })
+ })
+ resultPipe.put(future)
+ }
+ resultPipe.put(endOfStreamMarker)
+ }
+ } else {
+ val pool = Executors.newFixedThreadPool(ThreadCount) {
+ Thread(it).apply { isDaemon = true }
+ }
+ while (iterator.hasNext()) {
+ val source = ArrayList<T>(chunkSize)
+ while (source.size < chunkSize && iterator.hasNext()) {
+ source.add(iterator.next())
+ }
+ resultPipe.put(pool.submit( Callable {
+ val state = threadLocal.get()
+ source.map { map(state, it) }
+ }))
+ }
+ resultPipe.put(endOfStreamMarker)
+ }
+ }
+
+ return object : Iterator<U> {
+ var current: Iterator<U>? = null
+
+ init {
+ takeNext()
+ }
+
+ private fun takeNext() {
+ val next = resultPipe.take()
+ if (next != endOfStreamMarker) {
+ current = next.get().iterator()
+ }
+ }
+
+ override fun next(): U {
+ val ret = current!!.next()
+ if (!current!!.hasNext()) {
+ current = null
+ takeNext()
+ }
+ return ret
+ }
+
+ override fun hasNext(): Boolean {
+ return current != null
+ }
+
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/util/BufferReader.kt b/core/common/src/main/kotlin/trebuchet/util/BufferReader.kt
new file mode 100644
index 0000000..800c07b
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/util/BufferReader.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+@file:Suppress("ConvertTwoComparisonsToRangeCheck", "NOTHING_TO_INLINE")
+
+package trebuchet.util
+
+import trebuchet.io.DataSlice
+import java.util.regex.Matcher
+
+inline fun Byte.isDigit() = this >= '0'.toByte() && this <= '9'.toByte()
+
+open class BufferReaderState(var buffer: ByteArray, var index: Int, var endIndexExclusive: Int) {
+ constructor() : this(DataSlice.EmptyBuffer, 0, 0)
+
+ inline fun peek() = buffer[index]
+
+ inline fun isDigit() = buffer[index].isDigit()
+
+ inline fun skip() {
+ index++
+ }
+
+ inline fun skipCount(count: Int) {
+ index += count
+ }
+
+ inline fun skipChar(char: Byte) {
+ while (index < endIndexExclusive && buffer[index] == char) { index++ }
+ }
+
+ inline fun skipSingle(char: Byte) {
+ if (peek() == char) skip()
+ }
+
+ inline fun skipUntil(cb: (Byte) -> Boolean) {
+ while (index < endIndexExclusive && !cb(peek())) { index++ }
+ }
+
+ inline fun end() { index = endIndexExclusive }
+
+ inline fun skipTo(search: StringSearch) {
+ val foundAt = search.find(buffer, index, endIndexExclusive)
+ index = if (foundAt != -1) foundAt else endIndexExclusive
+ }
+
+ inline fun readByte() = buffer[index++]
+}
+
+class PreviewReader : BufferReaderState() {
+ val startIndex = index
+ inline fun rewind() {
+ index--
+ if (index < startIndex) { throw IndexOutOfBoundsException() }
+ }
+ inline fun rewindUntil(cb: (Byte) -> Boolean) {
+ while (!cb(peek())) { rewind() }
+ }
+}
+
+class MatchResult(val reader: BufferReader) {
+ var matcher: Matcher? = null
+ var startIndex: Int = 0
+
+ fun int(group: Int): Int {
+ reader.index = startIndex + matcher!!.start(group)
+ return reader.readInt()
+ }
+
+ fun intOr(group: Int, default: Int): Int {
+ return if (matcher!!.start(group) == -1) default else int(group)
+ }
+
+ fun double(group: Int): Double {
+ reader.index = startIndex + matcher!!.start(group)
+ return reader.readDouble()
+ }
+
+ fun long(group: Int): Long {
+ reader.index = startIndex + matcher!!.start(group)
+ return reader.readLong()
+ }
+
+ fun string(group: Int): String {
+ reader.index = startIndex + matcher!!.start(group)
+ val endAt = startIndex + matcher!!.end(group)
+ return reader.stringTo { index = endAt }
+ }
+
+ fun slice(group: Int): DataSlice {
+ reader.index = startIndex + matcher!!.start(group)
+ val endAt = startIndex + matcher!!.end(group)
+ return reader.sliceTo { index = endAt }
+ }
+
+ fun reader(group: Int): BufferReader {
+ reader.index = startIndex + matcher!!.start(group)
+ return reader
+ }
+
+ fun <T> read(group: Int, cb: PreviewReader.() -> T): T {
+ val tempPreview = reader.tempPreview
+ tempPreview.buffer = reader.buffer
+ tempPreview.index = startIndex + matcher!!.start(group)
+ tempPreview.endIndexExclusive = startIndex + matcher!!.end(group)
+ return cb.invoke(tempPreview)
+ }
+}
+
+class BufferReader : BufferReaderState() {
+ companion object {
+ val PowerOf10s = intArrayOf(1, 10, 100, 1000, 10_000, 100_000, 1_000_000)
+ }
+
+ var stringCache: StringCache? = null
+ val tempSlice = DataSlice()
+ val tempPreview = PreviewReader()
+ val tempMatchResult = MatchResult(this)
+
+ val charWrapper = object : CharSequence {
+ override val length: Int
+ get() = endIndexExclusive - index
+
+ override fun get(index: Int): Char {
+ return buffer[this@BufferReader.index + index].toChar()
+ }
+
+ override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
+ TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+ }
+
+ }
+
+ fun reset(slice: DataSlice, stringCache: StringCache?) {
+ this.buffer = slice.buffer
+ this.index = slice.startIndex
+ this.endIndexExclusive = slice.endIndex
+ this.stringCache = stringCache
+ }
+
+ inline fun <T> read(slice: DataSlice, stringCache: StringCache? = null, init: BufferReader.() -> T): T {
+ this.reset(slice, stringCache)
+ val ret = this.init()
+ this.stringCache = null
+ return ret
+ }
+
+ fun readInt(): Int {
+ return readLong().toInt();
+ }
+
+ fun readLong(): Long {
+ var value: Long = 0
+ var foundDigit = false
+ val startIndex = index
+ while (index < endIndexExclusive) {
+ val c = buffer[index] - '0'.toByte()
+ if (c >= 0 && c <= 9) {
+ foundDigit = true
+ value *= 10
+ value += c
+ } else if (foundDigit) {
+ return value
+ }
+ index++
+ }
+ if (foundDigit) {
+ return value
+ }
+ throw NumberFormatException(
+ "${String(buffer, startIndex, index - startIndex)} is not an int")
+ }
+
+ fun readDouble(): Double {
+ skipUntil { it == '.'.toByte() || isDigit() }
+ var result = readInt().toDouble()
+ if (peek() == '.'.toByte()) {
+ skip()
+ val startI = index
+ val second = readInt().toDouble()
+ val magnitude = index - startI
+ result += (second / PowerOf10s[magnitude])
+ }
+ return result
+ }
+
+ inline fun stringTo(init: PreviewReader.() -> Unit): String {
+ val slice = sliceTo(tempSlice, init)
+ return stringCache?.stringFor(slice) ?: slice.toString()
+ }
+
+ inline fun sliceTo(slice: DataSlice = DataSlice(), init: PreviewReader.() -> Unit): DataSlice {
+ tempPreview.buffer = buffer
+ tempPreview.index = index
+ tempPreview.endIndexExclusive = endIndexExclusive
+ tempPreview.init()
+ slice.set(buffer, index, tempPreview.index)
+ index = tempPreview.index
+ return slice
+ }
+
+ inline fun match(matcher: Matcher, result: MatchResult.() -> Unit) {
+ if (!tryMatch(matcher, result)) {
+ println("RE failed on '${stringTo { end() }}'")
+ }
+ }
+
+ inline fun tryMatch(matcher: Matcher, result: MatchResult.() -> Unit): Boolean {
+ matcher.reset(charWrapper)
+ if (matcher.matches()) {
+ tempMatchResult.matcher = matcher
+ tempMatchResult.startIndex = index
+ result.invoke(tempMatchResult)
+ tempMatchResult.matcher = null
+ return true
+ }
+ return false
+ }
+}
+
diff --git a/core/common/src/main/kotlin/trebuchet/util/Builders.kt b/core/common/src/main/kotlin/trebuchet/util/Builders.kt
new file mode 100644
index 0000000..932392b
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/util/Builders.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.util
+
+import kotlin.reflect.full.createInstance
+
+private fun <T> noReset(t: T) = t
+
+class StartEndBuilder<R, out P>(
+ private val new: () -> P,
+ private val reset: (P) -> P = ::noReset) {
+
+ private val stack = mutableListOf<P>()
+ private val garbage = mutableListOf<P>()
+
+ fun start(cb: P.() -> Unit) {
+ val p = if (garbage.isNotEmpty())
+ garbage.removeAt(garbage.lastIndex)
+ else
+ new()
+ stack.add(p)
+ cb.invoke(p)
+ }
+
+ fun end(cb: P.() -> R): R? {
+ if (stack.isEmpty()) return null
+ val p = stack.removeAt(stack.lastIndex)
+ val ret = cb.invoke(p)
+ garbage.add(reset(p))
+ return ret
+ }
+
+ companion object {
+ inline fun <R, reified P : Any> make() : StartEndBuilder<R, P> {
+ return StartEndBuilder(P::class::createInstance)
+ }
+ }
+}
diff --git a/core/common/src/main/kotlin/trebuchet/util/ByteArrayList.kt b/core/common/src/main/kotlin/trebuchet/util/ByteArrayList.kt
new file mode 100644
index 0000000..9258c7c
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/util/ByteArrayList.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.util
+
+import trebuchet.io.DataSlice
+import trebuchet.io.asSlice
+
+class ByteArrayList constructor(initialCapacity: Int = 10) {
+
+ var size: Int = 0
+ private set
+
+ var capacity: Int
+ get() = array.size
+ set(value) {
+ if (value > capacity) {
+ array = array.copyOf(maxOf(value, (capacity * 1.5).toInt()))
+ }
+ }
+
+ private var array: ByteArray = ByteArray(initialCapacity)
+
+ operator fun get(index: Int): Byte {
+ return array[index]
+ }
+
+ fun put(b: Byte) {
+ capacity = size + 1
+ array[size] = b
+ size++
+ }
+
+ fun put(buf: ByteArray, offset: Int, length: Int) {
+ capacity = size + length
+ System.arraycopy(buf, offset, array, size, length)
+ size += length
+ }
+
+ fun put(slice: DataSlice) {
+ put(slice.buffer, slice.startIndex, slice.length)
+ }
+
+ fun reset(initialCapacity: Int = 10): DataSlice {
+ val slice = array.asSlice(size)
+ array = ByteArray(initialCapacity)
+ size = 0
+ return slice
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/util/PrintlnImportFeedback.kt b/core/common/src/main/kotlin/trebuchet/util/PrintlnImportFeedback.kt
new file mode 100644
index 0000000..08a1381
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/util/PrintlnImportFeedback.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.util
+
+import trebuchet.importers.ImportFeedback
+
+
+class PrintlnImportFeedback : ImportFeedback {
+ override fun reportImportException(exception: Throwable) {
+ println("Exception: ${exception.message}")
+ }
+
+ override fun reportImportWarning(warning: String) {
+ println(warning)
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/util/StringCache.kt b/core/common/src/main/kotlin/trebuchet/util/StringCache.kt
new file mode 100644
index 0000000..4b7e2c8
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/util/StringCache.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.util
+
+import trebuchet.io.DataSlice
+import java.util.concurrent.ConcurrentHashMap
+
+class StringCache {
+ // we're gonna have a lot of strings probably
+ private val cache: MutableMap<DataSlice, String> = HashMap(1_000)
+
+ fun stringFor(slice: DataSlice): String {
+ var ret = cache[slice]
+ if (ret == null) {
+ ret = slice.toString()
+ cache.putIfAbsent(slice.compact(), ret)
+ }
+ return ret
+ }
+} \ No newline at end of file
diff --git a/core/common/src/main/kotlin/trebuchet/util/StringSearch.kt b/core/common/src/main/kotlin/trebuchet/util/StringSearch.kt
new file mode 100644
index 0000000..2d9e3e3
--- /dev/null
+++ b/core/common/src/main/kotlin/trebuchet/util/StringSearch.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.util
+
+import trebuchet.io.GenericByteBuffer
+import trebuchet.io.StreamingReader
+
+/**
+ * Partial implementation of the Boyer-Moore string search algorithm.
+ * Requires that the bytearray to look for is <= 127 in length due to the use of
+ * signed bytes for the jump table.
+ */
+class StringSearch(val lookFor: String) {
+ val skipLut = ByteArray(256) { lookFor.length.toByte() }
+ val suffixSkip = ByteArray(lookFor.length)
+
+ private companion object {
+ fun isPrefix(str: CharSequence, pos: Int): Boolean {
+ return (0 until (str.length - pos)).none { str[it] != str[pos + it] }
+ }
+
+ fun longestCommonSuffix(word: CharSequence, pos: Int): Int {
+ var i: Int = 0
+ while (word[pos - i] == word[word.length - 1 - i] && i < pos) {
+ i++
+ }
+ return i
+ }
+ }
+
+ init {
+ val last = lookFor.length - 1
+ for (i in 0..last - 1) {
+ skipLut[lookFor[i].toInt() and 0xFF] = (last - i).toByte()
+ }
+
+ var lastPrefix = last
+ for (i in last downTo 0) {
+ if (isPrefix(lookFor, i + 1)) {
+ lastPrefix = i + 1
+ }
+ suffixSkip[i] = (lastPrefix + last - i).toByte()
+ }
+ for (i in 0 until last) {
+ val suffixLength = longestCommonSuffix(lookFor, i)
+ if(lookFor[i - suffixLength] != lookFor[last - suffixLength]) {
+ suffixSkip[last - suffixLength] = (suffixLength + last - i).toByte()
+ }
+ }
+ }
+
+ val length get() = lookFor.length
+
+ fun find(reader: StreamingReader, startIndex: Long = 0, inEndIndex: Long = Long.MAX_VALUE): Long {
+ var index = startIndex + lookFor.length - 1
+ var endIndex = inEndIndex
+ while (index <= endIndex) {
+ if (index > reader.endIndex) {
+ if (!reader.loadIndex(index)) return -1
+ if (reader.reachedEof) {
+ endIndex = reader.endIndex
+ }
+ }
+ // Search the overlapping region slowly
+ while (reader.windowFor(index) !== reader.windowFor(index - lookFor.length + 1)) {
+ var lookForIndex = lookFor.length - 1
+ while (lookForIndex >= 0 && reader[index] == lookFor[lookForIndex].toByte()) {
+ index--
+ lookForIndex--
+ }
+ if (lookForIndex < 0) {
+ return index + 1
+ }
+ index += maxOf(skipLut[reader[index].toInt() and 0xFF], suffixSkip[lookForIndex])
+
+ if (index > reader.endIndex) {
+ if (!reader.loadIndex(index)) return -1
+ if (reader.reachedEof) {
+ endIndex = reader.endIndex
+ }
+ }
+ }
+ // Now search the non-overlapping quickly
+ val window = reader.windowFor(index)
+ index = findInWindow(window, index, endIndex)
+ if (index <= window.globalEndIndex) {
+ // Found a match
+ return index
+ }
+ }
+ return -1
+ }
+
+ fun findInLoadedRegion(reader: StreamingReader, endIndex: Long = reader.endIndex): Long {
+ var index = reader.startIndex + lookFor.length - 1
+ while (index <= endIndex) {
+ // Search the overlapping region slowly
+ while (reader.windowFor(index) !== reader.windowFor(index - lookFor.length + 1)) {
+ var lookForIndex = lookFor.length - 1
+ while (lookForIndex >= 0 && reader[index] == lookFor[lookForIndex].toByte()) {
+ index--
+ lookForIndex--
+ }
+ if (lookForIndex < 0) {
+ return index + 1
+ }
+ index += maxOf(skipLut[reader[index].toInt() and 0xFF], suffixSkip[lookForIndex])
+ }
+ // Now search the non-overlapping quickly
+ val window = reader.windowFor(index)
+ index = findInWindow(window, index, endIndex)
+ if (index < window.globalEndIndex && index < endIndex) {
+ // Found a match
+ return index
+ }
+ }
+ return -1
+ }
+
+ fun find(buffer: GenericByteBuffer, startIndex: Long = 0, endIndex: Long = buffer.length): Long {
+ var index = startIndex + lookFor.length - 1
+ while (index < endIndex) {
+ var lookForIndex = lookFor.length - 1
+ while (lookForIndex >= 0 && buffer[index] == lookFor[lookForIndex].toByte()) {
+ index--
+ lookForIndex--
+ }
+ if (lookForIndex < 0) {
+ index += 1
+ break
+ }
+ index += maxOf(skipLut[buffer[index].toInt() and 0xFF], suffixSkip[lookForIndex])
+ }
+ return index
+ }
+
+ fun find(buffer: ByteArray, startIndex: Int = 0, endIndex: Int = buffer.size): Int {
+ var index = startIndex + lookFor.length - 1
+ while (index < endIndex) {
+ var lookForIndex = lookFor.length - 1
+ while (lookForIndex >= 0 && buffer[index] == lookFor[lookForIndex].toByte()) {
+ index--
+ lookForIndex--
+ }
+ if (lookForIndex < 0) {
+ index += 1
+ break
+ }
+ index += maxOf(skipLut[buffer[index].toInt() and 0xFF], suffixSkip[lookForIndex])
+ }
+ return index
+ }
+
+ private fun findInWindow(window: StreamingReader.Window, globalStartIndex: Long, globalEndIndex: Long): Long {
+ var index = (globalStartIndex - window.globalStartIndex).toInt()
+ val buffer = window.slice
+ val endIndex = (minOf(window.globalEndIndex, globalEndIndex) - window.globalStartIndex).toInt()
+ while (index <= endIndex) {
+ var lookForIndex = lookFor.length - 1
+ while (lookForIndex >= 0 && buffer[index] == lookFor[lookForIndex].toByte()) {
+ index--
+ lookForIndex--
+ }
+ if (lookForIndex < 0) {
+ index += 1
+ break
+ }
+ index += maxOf(skipLut[buffer[index].toInt() and 0xFF], suffixSkip[lookForIndex])
+ }
+ return index + window.globalStartIndex
+ }
+}
+
+fun searchFor(str: String) = StringSearch(str)
+
+fun GenericByteBuffer.indexOf(str: String, endIndex: Long = this.length): Long {
+ val stopAt = minOf(endIndex, this.length - 1)
+ if (this is StreamingReader) {
+ return StringSearch(str).findInLoadedRegion(this, stopAt)
+ }
+ return StringSearch(str).find(this, 0, stopAt)
+}
+
+fun GenericByteBuffer.contains(str: String, endIndex: Long = this.length): Boolean {
+ return this.indexOf(str, endIndex) != -1L
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/extractors/SystraceExtractorTest.kt b/core/common/src/test/kotlin/trebuchet/extractors/SystraceExtractorTest.kt
new file mode 100644
index 0000000..c4ea42c
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/extractors/SystraceExtractorTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extractors
+
+import org.junit.Test
+import org.junit.Assert.*
+import trebuchet.io.BufferProducer
+import trebuchet.io.DataSlice
+import trebuchet.io.StreamingReader
+import trebuchet.io.asSlice
+
+
+class SystraceExtractorTest {
+ val HTML_HEADER = "<!DOCTYPE html>\n<html>\n<head>"
+ val TRACE_BEGIN = """<script class="trace-data" type="application/text">"""
+ val TRACE_END = "</script>"
+
+ @Test
+ fun testExtractMultiWindowSingleOutput() {
+ val buffers = listOf(HTML_HEADER, TRACE_BEGIN, "one", TRACE_END)
+ val result = extract(buffers)
+ assertEquals(1, result.size)
+ assertEquals("one", result[0].toString())
+ }
+
+ @Test
+ fun testExtractMultiWindowMultiOutput() {
+ val buffers = listOf(HTML_HEADER, TRACE_BEGIN, "first", "second", "third", TRACE_END)
+ val result = extract(buffers)
+ assertEquals(3, result.size)
+ assertEquals("first", result[0].toString())
+ assertEquals("second", result[1].toString())
+ assertEquals("third", result[2].toString())
+ }
+
+ @Test
+ fun testExtractExtremeSplitting() {
+ val buffers = listOf(HTML_HEADER, TRACE_BEGIN, "12345", TRACE_END)
+ .joinToString(separator = "").asIterable().map { it.toString() }
+ val result = extract(buffers)
+// assertEquals(5, result.size)
+ assertEquals("1", result[0].toString())
+ assertEquals("2", result[1].toString())
+ assertEquals("3", result[2].toString())
+ assertEquals("4", result[3].toString())
+ assertEquals("5", result[4].toString())
+ }
+
+ @Test
+ fun testExtractSingleBuffer() {
+ val buffer = listOf(HTML_HEADER, TRACE_BEGIN, "one big buffer", TRACE_END)
+ .joinToString(separator = "")
+ val result = extract(listOf(buffer))
+ assertEquals(1, result.size)
+ assertEquals("one big buffer", result[0].toString())
+ }
+
+ fun extract(list: Iterable<String>): List<DataSlice> {
+ val extractor = SystraceExtractor()
+ var output = mutableListOf<DataSlice>()
+ extractor.extract(makeStream(list.iterator())) { outputStream ->
+ while (true) {
+ val next = outputStream.next()
+ if (next == null) {
+ outputStream.close()
+ break
+ }
+ output.add(next)
+ }
+ }
+ return output
+ }
+
+ fun makeStream(list: Iterator<String>): StreamingReader {
+ return StreamingReader(object : BufferProducer {
+ override fun next(): DataSlice? {
+ if (list.hasNext()) {
+ return list.next().asSlice()
+ }
+ return null
+ }
+ })
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/extractors/ZlibExtractorTest.kt b/core/common/src/test/kotlin/trebuchet/extractors/ZlibExtractorTest.kt
new file mode 100644
index 0000000..d773f77
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/extractors/ZlibExtractorTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.extractors
+
+import org.junit.Test
+import trebuchet.extras.InputStreamAdapter
+import trebuchet.extras.findSampleData
+import trebuchet.importers.FatalImportFeedback
+import trebuchet.io.BufferProducer
+import trebuchet.io.StreamingReader
+import java.io.File
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ZlibExtractorTest {
+ @Test
+ fun testFactorySuccess() {
+ val reader = StreamingReader(openSample())
+ reader.loadIndex(reader.keepLoadedSize.toLong())
+ assertNotNull(ZlibExtractor.Factory.extractorFor(reader, FatalImportFeedback))
+ }
+
+ @Test
+ fun testFactoryNotDeflated() {
+ val reader = StreamingReader(openSample("sample.ftrace"))
+ reader.loadIndex(reader.keepLoadedSize.toLong())
+ assertNull(ZlibExtractor.Factory.extractorFor(reader, FatalImportFeedback))
+ }
+
+ @Test
+ fun testFactoryNoHeader() {
+ val reader = StreamingReader(openSample("caltrace1.html"))
+ reader.loadIndex(reader.keepLoadedSize.toLong())
+ assertNull(ZlibExtractor.Factory.extractorFor(reader, FatalImportFeedback))
+ }
+
+ @Test
+ fun testExtractInitial() {
+ val expected = "# tracer: nop\n#\n# ent"
+ val extractor = ZlibExtractor(FatalImportFeedback)
+ extractor.extract(StreamingReader(openSample())) {
+ val first = it.next()
+ assertNotNull(first)
+ assertTrue(first!!.length > expected.length)
+ assertEquals(expected, first.slice(0, expected.length).toString())
+ it.close()
+ }
+ }
+
+ private fun openSample(name: String = "missed_traces.trace"): BufferProducer {
+ val file = File(findSampleData(), name)
+ assertTrue(file.exists(), "Unable to find '$name'")
+ return InputStreamAdapter(file)
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/importers/DummyImportFeedback.kt b/core/common/src/test/kotlin/trebuchet/importers/DummyImportFeedback.kt
new file mode 100644
index 0000000..1a24d71
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/importers/DummyImportFeedback.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers
+
+import org.junit.Assert.fail
+
+object DummyImportFeedback : ImportFeedback {
+ override fun reportImportException(exception: Throwable) { }
+
+ override fun reportImportWarning(warning: String) { }
+}
+
+object FatalImportFeedback : ImportFeedback {
+ override fun reportImportException(exception: Throwable) {
+ throw exception
+ }
+
+ override fun reportImportWarning(warning: String) {
+ fail(warning)
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/importers/ftrace/EventTestHelpers.kt b/core/common/src/test/kotlin/trebuchet/importers/ftrace/EventTestHelpers.kt
new file mode 100644
index 0000000..c7e6ed6
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/importers/ftrace/EventTestHelpers.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import trebuchet.importers.ftrace.events.FtraceEvent
+import trebuchet.importers.ftrace.events.FtraceEventDetails
+import trebuchet.importers.ftrace.events.createEventParser
+import trebuchet.io.asSlice
+import kotlin.test.fail
+
+fun parseEvent(line: String): FtraceEvent {
+ return FtraceEvent.tryParseText(createEventParser(),
+ line.asSlice()) ?: fail("Failed to parseEvent event '${line}'")
+}
+
+inline fun <reified T : FtraceEventDetails> detailsFor(details: String): T {
+ val event = parseEvent("atrace-7100 ( 7100) [001] ...1 4492.047398: $details")
+ return event.details as? T
+ ?: fail("Details expected ${T::class.simpleName}, got ${event.details::class.simpleName}")
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceEventTest.kt b/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceEventTest.kt
new file mode 100644
index 0000000..9ace989
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceEventTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import trebuchet.importers.ftrace.events.*
+import trebuchet.io.asSlice
+import trebuchet.model.InvalidId
+import trebuchet.model.SchedulingState
+import kotlin.test.*
+
+class FtraceEventTest {
+
+ /** Tests a 3.2+ ftrace line with tgid and no missing data */
+ @Test fun testParseLineComplete() {
+ val event = parseEvent("atrace-7100 ( 7100) [001] ...1 4492.047398: tracing_mark_write: " +
+ "trace_event_clock_sync: parent_ts=4492.069824")
+ assertEquals("atrace", event.task)
+ assertEquals(7100, event.pid)
+ assertEquals(7100, event.tgid)
+ assertEquals(1, event.cpu)
+ assertEquals(4492.047398, event.timestamp)
+ assertEquals("tracing_mark_write", event.function)
+ assertTrue { event.details is ParentTSEvent }
+ assertEquals(4492.069824, (event.details as ParentTSEvent).parentTimestamp)
+ }
+
+ /** Tests a 3.2+ ftrace line with tgid option but missing tgid & task name */
+ @Test fun testParseLineUnknownTgid() {
+ val event = parseEvent("<...>-2692 (-----) [000] d..4 4492.062904: sched_blocked_reason: pid=2694 " +
+ "iowait=0 caller=qpnp_vadc_conv_seq_request+0x64/0x794")
+ assertNull(event.task)
+ assertEquals(2692, event.pid)
+ assertEquals(InvalidId, event.tgid)
+ assertEquals(0, event.cpu)
+ assertEquals(4492.062904, event.timestamp)
+ assertEquals("sched_blocked_reason", event.function)
+ }
+
+ /** Tests a 3.2+ ftrace line without tgid option */
+ @Test fun testParseLineNoTgid() {
+ val event = parseEvent("atrace-7100 [001] d..3 4492.047432: sched_switch: prev_comm=atrace prev_pid=7100 " +
+ "prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120")
+ assertEquals("atrace", event.task)
+ assertEquals(7100, event.pid)
+ assertEquals(InvalidId, event.tgid)
+ assertEquals(1, event.cpu)
+ assertEquals(4492.047432, event.timestamp)
+ assertEquals("sched_switch", event.function)
+ val details = event.details as? SchedSwitchEvent ?: fail("${event.details} not instance of SchedSwitchEvent")
+ assertEquals("atrace", details.prevComm)
+ assertEquals(7100, details.prevPid)
+ assertEquals(120, details.prevPrio)
+ assertEquals(SchedulingState.SLEEPING, details.prevState)
+ assertEquals("swapper/1", details.nextComm)
+ assertEquals(0, details.nextPid)
+ assertEquals(120, details.nextPrio)
+ }
+
+ /** Tests a pre-3.2 ftrace line */
+ @Test fun testParseLineLegacy() {
+ val event = parseEvent("Binder-Thread #-647 [001] 260.464294: sched_switch: prev_pid=0")
+ assertEquals("Binder-Thread #", event.task)
+ assertEquals(647, event.pid)
+ assertEquals(InvalidId, event.tgid)
+ assertEquals(1, event.cpu)
+ assertEquals(260.464294, event.timestamp)
+ assertEquals("sched_switch", event.function)
+ assertSame(NoDetails, event.details)
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceImporterTest.kt b/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceImporterTest.kt
new file mode 100644
index 0000000..d0cc7ea
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceImporterTest.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import org.junit.Assert.*
+import org.junit.Test
+import trebuchet.importers.FatalImportFeedback
+import trebuchet.io.StreamingReader
+import trebuchet.model.Model
+import trebuchet.testutils.makeReader
+
+class FtraceImporterTest {
+
+ fun String.makeLoadedReader(): StreamingReader {
+ val reader = this.makeReader()
+ reader.loadIndex(reader.keepLoadedSize.toLong())
+ return reader
+ }
+
+ @Test fun testImporterFor() {
+ val line1 = "atrace-7100 ( 7100) [001] ...1 4492.047398: tracing_mark_write: trace_event_clock_sync: parent_ts=4492.069824"
+ val line2 = "<idle>-0 (-----) [001] dN.4 4492.047448: sched_wakeup: comm=ksoftirqd/1 pid=15 prio=120 success=1 target_cpu=001"
+ val traceData = withHeader(line1, line2)
+ assertNotNull(FtraceImporter.Factory.importerFor(HEADER.makeLoadedReader(), FatalImportFeedback))
+ assertNotNull(FtraceImporter.Factory.importerFor(traceData.makeLoadedReader(), FatalImportFeedback))
+ assertNull(FtraceImporter.Factory.importerFor(HEADER.makeReader(), FatalImportFeedback))
+ assertNull(FtraceImporter.Factory.importerFor(traceData.makeReader(), FatalImportFeedback))
+ assertNull(FtraceImporter.Factory.importerFor("Hello, World!".makeLoadedReader(), FatalImportFeedback))
+ assertNull(FtraceImporter.Factory.importerFor(line1.makeLoadedReader(), FatalImportFeedback))
+ }
+
+ @Test fun testImporterTimestamp() {
+ val traceData = withHeader(
+ "equicksearchbox-6381 ( 6381) [004] ...1 4493.734816: tracing_mark_write: trace_event_clock_sync: parent_ts=23816.083984",
+ "equicksearchbox-6381 ( 6381) [004] ...1 4493.734855: tracing_mark_write: trace_event_clock_sync: realtime_ts=1491850748338")
+ val importer = FtraceImporter(FatalImportFeedback)
+ val modelFragment = importer.import(traceData.makeReader())
+ assertNotNull(modelFragment)
+ if (modelFragment == null) return // just to make Kotlin happy
+ assertEquals(23816.083984, modelFragment.parentTimestamp, .001)
+ assertEquals(1491850748338, modelFragment.realtimeTimestamp)
+ }
+
+ @Test fun testImportBeginEnd() {
+ val traceData = withHeader(
+ " equicksearchbox-6381 ( 6381) [004] ...1 4493.734816: tracing_mark_write: E",
+ " equicksearchbox-6381 ( 6381) [005] ...1 4493.730786: tracing_mark_write: B|6381|Choreographer#doFrame",
+ " equicksearchbox-6381 ( 6381) [005] ...1 4493.730824: tracing_mark_write: B|6381|input",
+ " equicksearchbox-6381 ( 6381) [005] ...1 4493.732287: tracing_mark_write: E",
+ " equicksearchbox-6381 ( 6381) [005] ...1 4493.732310: tracing_mark_write: B|6381|traversal",
+ " equicksearchbox-6381 ( 6381) [005] ...1 4493.732410: tracing_mark_write: B|6381|draw",
+ " equicksearchbox-6381 ( 6381) [004] ...1 4493.734816: tracing_mark_write: E",
+ " equicksearchbox-6381 ( 6381) [004] ...1 4493.734828: tracing_mark_write: E",
+ " equicksearchbox-6381 ( 6381) [004] ...1 4493.734855: tracing_mark_write: E")
+ val importer = FtraceImporter(FatalImportFeedback)
+ val modelFragment = importer.import(traceData.makeReader())
+ assertNotNull(modelFragment)
+ if (modelFragment == null) return // just to make Kotlin happy
+ assertEquals(1, modelFragment.processes.size)
+ val process = modelFragment.processes[0]
+ assertEquals(6381, process.id)
+ assertEquals("equicksearchbox", process.name)
+ assertEquals(1, process.threads.size)
+ val thread = process.threads.first()
+ assertEquals(6381, thread.id)
+ assertEquals("equicksearchbox", thread.name)
+ val sliceGroup = thread.slicesBuilder
+ assertFalse(sliceGroup.hasOpenSlices())
+ assertEquals(1, sliceGroup.slices.size)
+ val doFrameSlice = sliceGroup.slices[0]
+ assertEquals("Choreographer#doFrame", doFrameSlice.name)
+ assertEquals(2, doFrameSlice.children.size)
+ assertEquals("input", doFrameSlice.children[0].name)
+ assertEquals(0, doFrameSlice.children[0].children.size)
+ assertEquals("traversal", doFrameSlice.children[1].name)
+ assertEquals(1, doFrameSlice.children[1].children.size)
+ assertEquals("draw", doFrameSlice.children[1].children[0].name)
+ }
+
+ @Test fun testImportBeginEndNoTgids() {
+ val traceData = withHeader(
+ " equicksearchbox-6381 (-----) [004] ...1 4493.734828: tracing_mark_write: E",
+ " equicksearchbox-6381 (-----) [005] ...1 4493.730786: tracing_mark_write: B|6381|Choreographer#doFrame",
+ " equicksearchbox-6381 (-----) [005] ...1 4493.730824: tracing_mark_write: B|6381|input",
+ " equicksearchbox-6381 (-----) [005] ...1 4493.732287: tracing_mark_write: E",
+ " equicksearchbox-6381 (-----) [005] ...1 4493.732310: tracing_mark_write: B|6381|traversal",
+ " equicksearchbox-6381 (-----) [005] ...1 4493.732410: tracing_mark_write: B|6381|draw",
+ " equicksearchbox-6381 (-----) [004] ...1 4493.734816: tracing_mark_write: E",
+ " equicksearchbox-6381 (-----) [004] ...1 4493.734828: tracing_mark_write: E",
+ " equicksearchbox-6381 (-----) [004] ...1 4493.734855: tracing_mark_write: E")
+ val importer = FtraceImporter(FatalImportFeedback)
+ val modelFragment = importer.import(traceData.makeReader())
+ assertNotNull(modelFragment)
+ if (modelFragment == null) return // just to make Kotlin happy
+ assertEquals(1, modelFragment.processes.size)
+ val process = modelFragment.processes[0]
+ assertEquals(6381, process.id)
+ assertEquals("equicksearchbox", process.name)
+ assertEquals(1, process.threads.size)
+ val thread = process.threads.first()
+ assertEquals(6381, thread.id)
+ assertEquals("equicksearchbox", thread.name)
+ val sliceGroup = thread.slicesBuilder
+ assertFalse(sliceGroup.hasOpenSlices())
+ assertEquals(1, sliceGroup.slices.size)
+ val doFrameSlice = sliceGroup.slices[0]
+ assertEquals("Choreographer#doFrame", doFrameSlice.name)
+ assertEquals(2, doFrameSlice.children.size)
+ assertEquals("input", doFrameSlice.children[0].name)
+ assertEquals(0, doFrameSlice.children[0].children.size)
+ assertEquals("traversal", doFrameSlice.children[1].name)
+ assertEquals(1, doFrameSlice.children[1].children.size)
+ assertEquals("draw", doFrameSlice.children[1].children[0].name)
+ }
+
+ @Test fun testCounters() {
+ val model = parse(
+ " <...>-4932 (-----) [000] ...1 4493.660106: tracing_mark_write: C|3691|iq|1",
+ "InputDispatcher-4931 ( 3691) [000] ...1 4493.660790: tracing_mark_write: C|3691|iq|0")
+ assertEquals(1, model.processes.size)
+ val p = model.processes[3691]!!
+ assertEquals(3691, p.id)
+ assertEquals(3, p.threads.size)
+ assertTrue(p.threads.any { it.id == 3691 })
+ assertTrue(p.threads.any { it.id == 4931 })
+ assertTrue(p.threads.any { it.id == 4932 })
+ assertEquals(1, p.counters.size)
+ val c = p.counters[0]
+ assertEquals(2, c.events.size)
+ assertEquals(4493.660106, c.events[0].timestamp, .1)
+ assertEquals(1, c.events[0].count)
+ assertEquals(4493.660790, c.events[1].timestamp, .1)
+ assertEquals(0, c.events[1].count)
+
+ }
+
+ fun parse(vararg lines: String): Model {
+ val traceData = withHeader(*lines)
+ val importer = FtraceImporter(FatalImportFeedback)
+ val modelFragment = importer.import(traceData.makeReader())
+ assertNotNull(modelFragment)
+ return Model(modelFragment!!)
+ }
+
+ fun withHeader(vararg lines: String): String {
+ return lines.joinToString("\n", HEADER)
+ }
+
+ val HEADER = """TRACE:
+# tracer: nop
+#
+# entries-in-buffer/entries-written: 69580/69580 #P:8
+#
+# _-----=> irqs-off
+# / _----=> need-resched
+# | / _---=> hardirq/softirq
+# || / _--=> preempt-depth
+# ||| / delay
+# TASK-PID TGID CPU# |||| TIMESTAMP FUNCTION
+# | | | | |||| | |
+"""
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceLineTest.kt b/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceLineTest.kt
new file mode 100644
index 0000000..d74d45d
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/importers/ftrace/FtraceLineTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import org.junit.Assert.*
+import org.junit.Test
+import trebuchet.io.asSlice
+import trebuchet.model.InvalidId
+import trebuchet.util.StringCache
+
+class FtraceLineTest {
+
+ /** Tests a 3.2+ ftrace line with tgid and no missing data */
+ @Test fun testFtraceLineParser1() {
+ FtraceLine.Parser(StringCache()).parseLine(
+ ("atrace-7100 ( 7100) [001] d..3 4492.047432: sched_switch: prev_comm=atrace prev_pid=7100" +
+ " prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120").asSlice()) {
+ line ->
+ assertEquals("atrace", line.task)
+ assertEquals(7100, line.pid)
+ assertEquals(7100, line.tgid)
+ assertEquals(1, line.cpu)
+ assertEquals(4492.047432, line.timestamp, .1)
+ assertEquals("sched_switch", line.function.toString())
+ assertEquals("prev_comm=atrace prev_pid=7100 prev_prio=120 prev_state=S ==> next_comm=swapper/1" +
+ " next_pid=0 next_prio=120", line.functionDetailsReader.stringTo { end() })
+ }
+ }
+
+ /** Tests a 3.2+ ftrace line with tgid option but missing tgid & task name */
+ @Test fun testFtraceLineParser2() {
+ FtraceLine.Parser(StringCache()).parseLine((" <...>-2692 (-----) [000] d..4 4492.062904: " +
+ "sched_blocked_reason: pid=2694 iowait=0 caller=qpnp_vadc_conv_seq_request+0x64/0x794").asSlice()) {
+ line ->
+ assertNull(line.task)
+ assertEquals(2692, line.pid)
+ assertEquals(InvalidId, line.tgid)
+ assertEquals(0, line.cpu)
+ assertEquals(4492.062904, line.timestamp, .1)
+ assertEquals("sched_blocked_reason", line.function.toString())
+ assertEquals("pid=2694 iowait=0 caller=qpnp_vadc_conv_seq_request+0x64/0x794",
+ line.functionDetailsReader.stringTo { end() })
+ }
+ }
+
+ /** Tests a 3.2+ ftrace line without tgid option */
+ @Test fun testFtraceLineParser3() {
+ FtraceLine.Parser(StringCache()).parseLine(
+ ("atrace-7100 [001] d..3 4492.047432: sched_switch: prev_comm=atrace prev_pid=7100 " +
+ "prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120").asSlice()) {
+ line ->
+ assertEquals("atrace", line.task)
+ assertEquals(7100, line.pid)
+ assertEquals(InvalidId, line.tgid)
+ assertEquals(1, line.cpu)
+ assertEquals(4492.047432, line.timestamp, .1)
+ assertEquals("sched_switch", line.function.toString())
+ assertEquals("prev_comm=atrace prev_pid=7100 prev_prio=120 prev_state=S ==> next_comm=swapper/1" +
+ " next_pid=0 next_prio=120", line.functionDetailsReader.stringTo { end() })
+ }
+ }
+
+ /** Tests a pre-3.2 ftrace line */
+ @Test fun testFtraceLineParser4() {
+ FtraceLine.Parser(StringCache()).parseLine(
+ "Binder-Thread #-647 [001] 260.464294: sched_switch: prev_pid=0".asSlice()) {
+ line ->
+ assertEquals("Binder-Thread #", line.task)
+ assertEquals(647, line.pid)
+ assertEquals(InvalidId, line.tgid)
+ assertEquals(1, line.cpu)
+ assertEquals(260.464294, line.timestamp, .1)
+ assertEquals("sched_switch", line.function.toString())
+ assertEquals("prev_pid=0", line.functionDetailsReader.stringTo { end() })
+ }
+ }
+
+ @Test fun testFtraceLineParse5() {
+ FtraceLine.Parser(StringCache()).parseLine(("NearbyMessages-5675 ( 2303) [001] dn.4 23815.466030: "
+ + "sched_wakeup: comm=Binder:928_1 pid=945 prio=120 success=1 target_cpu=001").asSlice()) {
+ line ->
+ assertEquals("NearbyMessages", line.task)
+ assertEquals(5675, line.pid)
+ assertEquals(2303, line.tgid)
+ assertEquals(1, line.cpu)
+ assertEquals(23815.466030, line.timestamp, .1)
+ assertEquals("sched_wakeup", line.function.toString())
+ assertEquals("comm=Binder:928_1 pid=945 prio=120 success=1 target_cpu=001",
+ line.functionDetailsReader.stringTo { end() })
+ }
+ }
+
+ @Test fun testFtraceLineParse6() {
+ FtraceLine.Parser(StringCache()).parseLine(("NearbyMessages-56751 (12303) [001] dn.4 23815.466030: "
+ + "sched_wakeup: comm=Binder:928_1 pid=945 prio=120 success=1 target_cpu=001").asSlice()) {
+ line ->
+ assertEquals("NearbyMessages", line.task)
+ assertEquals(56751, line.pid)
+ assertEquals(12303, line.tgid)
+ assertEquals(1, line.cpu)
+ assertEquals(23815.466030, line.timestamp, .1)
+ assertEquals("sched_wakeup", line.function.toString())
+ assertEquals("comm=Binder:928_1 pid=945 prio=120 success=1 target_cpu=001",
+ line.functionDetailsReader.stringTo { end() })
+ }
+ }
+
+ @Test fun testFtraceLineParse7() {
+ FtraceLine.Parser(StringCache()).parseLine(("<9729>-9729 (-----) [003] ...1 23815.466141: "
+ + "tracing_mark_write: trace_event_clock_sync: parent_ts=23816.083984").asSlice()) {
+ line ->
+ assertEquals("<9729>", line.task)
+ assertEquals(9729, line.pid)
+ assertEquals(InvalidId, line.tgid)
+ assertEquals(3, line.cpu)
+ assertEquals(23815.466141, line.timestamp, .1)
+ assertEquals("tracing_mark_write", line.function.toString())
+ assertEquals("trace_event_clock_sync: parent_ts=23816.083984",
+ line.functionDetailsReader.stringTo { end() })
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/importers/ftrace/TracingMarkerEventTest.kt b/core/common/src/test/kotlin/trebuchet/importers/ftrace/TracingMarkerEventTest.kt
new file mode 100644
index 0000000..a40f289
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/importers/ftrace/TracingMarkerEventTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.importers.ftrace
+
+import trebuchet.importers.ftrace.events.*
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertSame
+
+class TracingMarkerEventTest {
+ @Test fun testBegin() {
+ val begin = detailsFor<BeginSliceEvent>("tracing_mark_write: B|5000|hello world")
+ assertEquals(5000, begin.tgid)
+ assertEquals("hello world", begin.title)
+ }
+
+ @Test fun testEnd() {
+ detailsFor<EndSliceEvent>("tracing_mark_write: E")
+ }
+
+ @Test fun testCounter() {
+ val counter = detailsFor<CounterSliceEvent>("tracing_mark_write: C|42|myCounter|9001")
+ assertEquals(42, counter.tgid)
+ assertEquals("myCounter", counter.name)
+ assertEquals(9001, counter.value)
+ }
+
+ @Test fun testParentTS() {
+ val parentTS = detailsFor<ParentTSEvent>(
+ "tracing_mark_write: trace_event_clock_sync: parent_ts=4492.069824")
+ assertEquals(4492.069824, parentTS.parentTimestamp)
+ }
+
+ @Test fun testRealtimeTS() {
+ val realtimeTS = detailsFor<RealtimeTSEvent>(
+ "tracing_mark_write: trace_event_clock_sync: realtime_ts=1491850748338")
+ assertEquals(1491850748338, realtimeTS.realtimeTimestamp)
+ }
+
+ @Test fun testUnknown() {
+ val details = detailsFor<FtraceEventDetails>("tracing_mark_write: this is unknown")
+ assertSame(NoDetails, details)
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/io/StringStreamTest.kt b/core/common/src/test/kotlin/trebuchet/io/StringStreamTest.kt
new file mode 100644
index 0000000..9b17206
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/io/StringStreamTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.io
+
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import trebuchet.extras.findSampleData
+import trebuchet.importers.DummyImportFeedback
+import trebuchet.model.Model
+import trebuchet.task.ImportTask
+import trebuchet.testutils.makeReader
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileReader
+import kotlin.test.assertNull
+
+class StringStreamTest {
+ @Test fun testLineReader() {
+ expect1_20_300("1\n20\n300")
+ }
+
+ @Test fun testLineReaderCLRF() {
+ expect1_20_300("1\r\n20\r\n300")
+ }
+
+ @Test fun testLineReaderOnFile() {
+ val file = File("${findSampleData()}/sample.ftrace")
+ Assert.assertTrue(file.exists())
+ val expected = BufferedReader(FileReader(file))
+ StreamingReader(readFile(file)).iterLines().forEach {
+ assertEquals(expected.readLine(), it.toString())
+ }
+ assertNull(expected.readLine())
+ }
+
+ @Test fun testLineReaderEmptyLines() {
+ expect1_20_300("\n\n1\r\n\r\n20\n\n\n\n300\n\n\n")
+
+ }
+
+ private fun expect1_20_300(data: String) {
+ val lines = mutableListOf<String>()
+ data.makeReader().iterLines().forEach {
+ lines.add(it.toString())
+ }
+ assertEquals(3, lines.size)
+ assertEquals("1", lines[0])
+ assertEquals("20", lines[1])
+ assertEquals("300", lines[2])
+ }
+
+ fun readFile(file: File): BufferProducer {
+ val inputStream = file.inputStream()
+ return object : BufferProducer {
+ override fun next(): DataSlice? {
+ val buffer = ByteArray(2 * 1024 * 1024)
+ val read = inputStream.read(buffer)
+ if (read == -1) return null
+ return buffer.asSlice(read)
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/model/SliceGroupBuilderTest.kt b/core/common/src/test/kotlin/trebuchet/model/SliceGroupBuilderTest.kt
new file mode 100644
index 0000000..70216a5
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/model/SliceGroupBuilderTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+import org.junit.Test
+import trebuchet.model.fragments.SliceGroupBuilder
+import kotlin.test.assertEquals
+
+class SliceGroupBuilderTest {
+ @Test
+ fun testSimpleBuild() {
+ val group = SliceGroupBuilder()
+ group.beginSlice {
+ it.startTime = 1.0
+ it.name = "first"
+ }
+ val slice = group.endSlice {
+ it.endTime = 2.0
+ }!!
+ assertEquals(1.0, slice.startTime)
+ assertEquals(2.0, slice.endTime)
+ assertEquals("first", slice.name)
+ assertEquals(slice, group.slices.first())
+ }
+
+ @Test fun testNestedBuild() {
+ val group = SliceGroupBuilder()
+ group.beginSlice {
+ it.startTime = 1.0
+ it.name = "first"
+ }
+ group.beginSlice {
+ it.startTime = 1.1
+ it.name = "nested"
+ }
+ val child = group.endSlice {
+ it.endTime = 1.2
+ }!!
+ assertEquals(1.1, child.startTime)
+ assertEquals(1.2, child.endTime)
+ assertEquals("nested", child.name)
+ assertEquals(0, group.slices.size)
+ val slice = group.endSlice {
+ it.endTime = 2.0
+ }!!
+ assertEquals(1.0, slice.startTime)
+ assertEquals(2.0, slice.endTime)
+ assertEquals("first", slice.name)
+ assertEquals(1, group.slices.size)
+ assertEquals(slice, group.slices.first())
+ assertEquals(1, slice.children.size)
+ assertEquals(child, slice.children.first())
+ assertEquals(0, child.children.size)
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/task/ImportTaskTest.kt b/core/common/src/test/kotlin/trebuchet/task/ImportTaskTest.kt
new file mode 100644
index 0000000..73e092a
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/task/ImportTaskTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.task
+
+import org.junit.Assert.*
+import org.junit.Test
+import trebuchet.extras.findSampleData
+import trebuchet.importers.DummyImportFeedback
+import trebuchet.io.BufferProducer
+import trebuchet.io.DataSlice
+import trebuchet.io.asSlice
+import trebuchet.model.Model
+import trebuchet.queries.SliceQueries
+import java.io.File
+
+class ImportTaskTest {
+ @Test fun testImportCameraTrace() {
+ val model = import("hdr-0608-4-trace.html")
+ val slices = SliceQueries.selectAll(model) { it.name.startsWith("MergeShot")}
+ assertEquals(2, slices.size)
+ assertEquals(0.868, slices[0].duration, .001)
+ assertEquals(0.866, slices[1].duration, .001)
+ }
+
+ @Test fun testImportCalTrace1() {
+ val model = import("caltrace1.html")
+ val counterName = "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#1"
+ val process = model.processes.values.find { it.name == "surfaceflinger" }!!
+ val thread = process.threads.find { it.name == "surfaceflinger" }!!
+ val slices = SliceQueries.selectAll(thread) { it.name == "handleMessageRefresh" }
+ assertEquals(103, slices.size)
+ assertFalse(slices.any { it.duration <= 0.0 })
+ val totalDuration = slices.map { it.duration }.reduce { a,b -> a+b }
+ assertEquals(.217984, totalDuration, .00001)
+ val counter = process.counters.find { it.name == counterName }
+ assertNotNull(counter)
+ counter!!
+ assertEquals(2, counter.events.filter { it.count == 2 }.size)
+ assertFalse(counter.events.any { it.count < 0 || it.count > 2})
+ }
+
+ @Test fun testImportSample() {
+ val model = import("sample.ftrace")
+ val process = model.processes[6381]!!
+ val thread = process.threads.find { it.name == "RenderThread" }!!
+ assertEquals(6506, thread.id)
+ }
+
+ private fun import(filename: String): Model {
+ val task = ImportTask(DummyImportFeedback)
+ val file = File(findSampleData(), filename)
+ assertTrue(file.exists())
+ val model = task.import(readFile(file))
+ assertFalse(model.isEmpty())
+ return model
+ }
+
+ fun readFile(file: File): BufferProducer {
+ val inputStream = file.inputStream()
+ return object : BufferProducer {
+ override fun next(): DataSlice? {
+ val buffer = ByteArray(2 * 1024 * 1024)
+ val read = inputStream.read(buffer)
+ if (read == -1) return null
+ return buffer.asSlice(read)
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/core/common/src/test/kotlin/trebuchet/testutils/StringToStreamAdapter.kt b/core/common/src/test/kotlin/trebuchet/testutils/StringToStreamAdapter.kt
new file mode 100644
index 0000000..08a1b87
--- /dev/null
+++ b/core/common/src/test/kotlin/trebuchet/testutils/StringToStreamAdapter.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.testutils
+
+import trebuchet.io.BufferProducer
+import trebuchet.io.DataSlice
+import trebuchet.io.StreamingReader
+import trebuchet.io.asSlice
+
+fun String.makeReader(): StreamingReader {
+ val source = this
+ return StreamingReader(object : BufferProducer {
+ var hasRead = false
+ override fun next(): DataSlice? {
+ if (hasRead) return null
+ hasRead = true
+ return source.asSlice()
+ }
+ })
+} \ No newline at end of file
diff --git a/core/model/build.gradle b/core/model/build.gradle
new file mode 100644
index 0000000..f3d59ce
--- /dev/null
+++ b/core/model/build.gradle
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+apply plugin: 'kotlin-platform-jvm'
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/Constants.kt b/core/model/src/main/kotlin/trebuchet/model/Constants.kt
new file mode 100644
index 0000000..4691599
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/Constants.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+const val InvalidId = -1 \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/Counter.kt b/core/model/src/main/kotlin/trebuchet/model/Counter.kt
new file mode 100644
index 0000000..431934e
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/Counter.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+import trebuchet.model.fragments.CounterFragment
+
+data class CounterValue(val timestamp: Double, val count: Int)
+
+class Counter(val name: String, val events: List<CounterValue>) {
+ constructor(fragment: CounterFragment) : this(fragment.name, fragment.events) {
+ events.sortedBy { it.timestamp }
+ }
+}
+
+infix fun Double.hasCount(value: Int): CounterValue = CounterValue(this, value) \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/CpuModel.kt b/core/model/src/main/kotlin/trebuchet/model/CpuModel.kt
new file mode 100644
index 0000000..c518c1c
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/CpuModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+package trebuchet.model
+
+import trebuchet.model.fragments.CpuModelFragment
+
+class CpuModel constructor(val model: Model, fragment: CpuModelFragment) {
+ val id: Int = fragment.id
+ val slices = fragment.slices
+ val hasContent = slices.isNotEmpty()
+
+ init {
+ if (id == InvalidId) throw IllegalArgumentException("Cpu has invalid id")
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/CpuProcessSlice.kt b/core/model/src/main/kotlin/trebuchet/model/CpuProcessSlice.kt
new file mode 100644
index 0000000..fa096b1
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/CpuProcessSlice.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+package trebuchet.model
+
+import trebuchet.model.base.Slice
+
+interface CpuProcessSlice : Slice {
+ val id: Int
+ val threadName: String
+ val threadId: Int
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/Model.kt b/core/model/src/main/kotlin/trebuchet/model/Model.kt
new file mode 100644
index 0000000..adadbb4
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/Model.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+import trebuchet.model.fragments.ModelFragment
+
+class Model constructor(fragments: Iterable<ModelFragment>) {
+ val processes: Map<Int, ProcessModel>
+ val cpus: List<CpuModel>
+ val beginTimestamp: Double
+ val endTimestamp: Double
+ val parentTimestamp: Double
+ val realtimeTimestamp: Long
+ val duration get() = endTimestamp - beginTimestamp
+
+ init {
+ val processBuilder = mutableMapOf<Int, ProcessModel>()
+ val cpuBuilder = mutableListOf<CpuModel>()
+ var beginTimestamp = Double.MAX_VALUE
+ var endTimestamp = 0.0
+ var parentTimestamp = 0.0
+ var realtimeTimestamp = 0L
+ fragments.forEach {
+ it.autoCloseOpenSlices()
+ beginTimestamp = minOf(beginTimestamp, it.globalStartTime)
+ endTimestamp = maxOf(endTimestamp, it.globalEndTime)
+ parentTimestamp = maxOf(parentTimestamp, it.parentTimestamp)
+ realtimeTimestamp = maxOf(realtimeTimestamp, it.realtimeTimestamp)
+ it.processes.forEach {
+ if (it.id != InvalidId) {
+ // TODO: Merge
+ processBuilder.put(it.id, ProcessModel(this, it))
+ }
+ }
+ it.cpus.forEach {
+ cpuBuilder.add(CpuModel(this, it))
+ }
+ }
+ cpuBuilder.sortBy { it.id }
+ processes = processBuilder
+ cpus = cpuBuilder
+ this.beginTimestamp = minOf(beginTimestamp, endTimestamp)
+ this.endTimestamp = endTimestamp
+ this.parentTimestamp = parentTimestamp
+ this.realtimeTimestamp = realtimeTimestamp
+ }
+
+ constructor(fragment: ModelFragment) : this(listOf(fragment))
+
+ fun isEmpty(): Boolean = processes.isEmpty()
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/ProcessModel.kt b/core/model/src/main/kotlin/trebuchet/model/ProcessModel.kt
new file mode 100644
index 0000000..8c48381
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/ProcessModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+import trebuchet.model.fragments.ProcessModelFragment
+
+class ProcessModel constructor(val model: Model, fragment: ProcessModelFragment) {
+ val id: Int = fragment.id
+ val name: String = fragment.name ?: "<$id>"
+ val threads: List<ThreadModel>
+ val counters: List<Counter>
+ val hasContent: Boolean
+
+ init {
+ if (id == InvalidId) throw IllegalArgumentException("Process has invalid id")
+ val threadBuilder = mutableListOf<ThreadModel>()
+ fragment.threads.forEach {
+ threadBuilder.add(ThreadModel(this, it))
+ }
+ threadBuilder.sortBy { it.id }
+ threads = threadBuilder
+ counters = fragment.counters.values.filter { it.events.isNotEmpty() }.map { Counter(it) }.toList()
+ hasContent = counters.isNotEmpty() || threads.any { it.hasContent }
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/SchedSlice.kt b/core/model/src/main/kotlin/trebuchet/model/SchedSlice.kt
new file mode 100644
index 0000000..2e652d7
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/SchedSlice.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+import trebuchet.model.base.Slice
+
+interface SchedSlice : Slice {
+ val state: SchedulingState
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/SchedulingState.kt b/core/model/src/main/kotlin/trebuchet/model/SchedulingState.kt
new file mode 100644
index 0000000..aecaaa7
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/SchedulingState.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+enum class SchedulingState(val friendlyName: String) {
+ DEBUG("Debug"),
+ EXIT_DEAD("Exit Dead"),
+ RUNNABLE("Runnable"),
+ RUNNING("Running"),
+ SLEEPING("Sleeping"),
+ STOPPED("Stopped"),
+ TASK_DEAD("Task Dead"),
+ UNINTR_SLEEP("Uninterruptible Sleep"),
+ UNINTR_SLEEP_WAKE_KILL("Uninterruptible Sleep | WakeKill"),
+ UNINTR_SLEEP_WAKING("Uninterruptible Sleep | Waking"),
+ UNINTR_SLEEP_IO("Uninterruptible Sleep - Block I/O"),
+ UNINTR_SLEEP_WAKE_KILL_IO("Uninterruptible Sleep | WakeKill - Block I/O"),
+ UNINTR_SLEEP_WAKING_IO("Uninterruptible Sleep | Waking - Block I/O"),
+ UNKNOWN("UNKNOWN"),
+ WAKE_KILL("Wakekill"),
+ WAKING("Waking"),
+ ZOMBIE("Zombie"),
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/ThreadModel.kt b/core/model/src/main/kotlin/trebuchet/model/ThreadModel.kt
new file mode 100644
index 0000000..c272ef2
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/ThreadModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model
+
+import trebuchet.model.fragments.ThreadModelFragment
+
+class ThreadModel constructor(val process: ProcessModel, fragment: ThreadModelFragment) {
+ val id: Int = fragment.id
+ val name: String = fragment.name ?: "<$id>"
+ val slices = fragment.slices
+ val schedSlices = fragment.schedSlices
+ val hasContent = slices.isNotEmpty() || schedSlices.isNotEmpty()
+
+ init {
+ if (id == InvalidId) throw IllegalArgumentException("Thread has invalid id")
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/base/Slice.kt b/core/model/src/main/kotlin/trebuchet/model/base/Slice.kt
new file mode 100644
index 0000000..04ea17b
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/base/Slice.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.base
+
+interface Slice {
+ /**
+ * Beginning of slice in seconds
+ */
+ val startTime: Double
+ /**
+ * End of slice in seconds
+ */
+ val endTime: Double
+ val name: String
+ val didNotFinish: Boolean
+
+ /**
+ * Total time taken by this slice in seconds
+ */
+ val duration: Double get() = endTime - startTime
+
+ /**
+ * Duration excluding time spend in children.
+ *
+ * The duration is in seconds.
+ */
+ val durationSelf: Double get() = duration
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/base/SliceGroup.kt b/core/model/src/main/kotlin/trebuchet/model/base/SliceGroup.kt
new file mode 100644
index 0000000..06ccd09
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/base/SliceGroup.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.base
+
+interface SliceGroup : Slice {
+ val children: List<SliceGroup>
+
+ /**
+ * Duration excluding time spend in children.
+ *
+ * The duration is in seconds.
+ */
+ override val durationSelf: Double get() {
+ var selfTime = duration
+ children.forEach { selfTime -= it.duration }
+ return selfTime
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/AutoCloseable.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/AutoCloseable.kt
new file mode 100644
index 0000000..005a05b
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/AutoCloseable.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+interface AutoCloseable {
+ fun autoClose(maxTimestamp: Double)
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/CounterFragment.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/CounterFragment.kt
new file mode 100644
index 0000000..e30e0b0
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/CounterFragment.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+import trebuchet.model.CounterValue
+
+class CounterFragment(val name: String) {
+ val events: MutableList<CounterValue> = mutableListOf()
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/CpuModelFragment.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/CpuModelFragment.kt
new file mode 100644
index 0000000..ab863ab
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/CpuModelFragment.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+import trebuchet.model.CpuProcessSlice
+import trebuchet.model.base.Slice
+
+
+class CpuModelFragment(var id: Int) {
+ val schedulingProcessBuilder = SchedulingProcessFragment.Builder()
+
+ val slices: List<CpuProcessSlice> get() {
+ return schedulingProcessBuilder.slices
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/ModelFragment.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/ModelFragment.kt
new file mode 100644
index 0000000..2a72441
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/ModelFragment.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+class ModelFragment {
+ val processes = mutableListOf<ProcessModelFragment>()
+ val cpus = mutableListOf<CpuModelFragment>()
+ var globalStartTime: Double = 0.0
+ var globalEndTime: Double = 0.0
+ var parentTimestamp: Double = 0.0
+ var realtimeTimestamp: Long = 0L
+
+ fun autoCloseOpenSlices() {
+ // And then close it
+ processes.forEach {
+ it.threads.forEach {
+ it.slicesBuilder.autoCloseOpenSlices(globalEndTime)
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/ProcessModelFragment.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/ProcessModelFragment.kt
new file mode 100644
index 0000000..f528ee5
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/ProcessModelFragment.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+import trebuchet.model.InvalidId
+import trebuchet.model.hasCount
+
+class ProcessModelFragment(id: Int, var name: String? = null,
+ private var hasIdCb: ((trebuchet.model.fragments.ProcessModelFragment) -> Unit)? = null) {
+ private var _id: Int = id
+ var id: Int
+ get() = _id
+ set(value) {
+ _id = value
+ if (_id != InvalidId) {
+ hasIdCb?.invoke(this)
+ hasIdCb = null
+ }
+ }
+
+ private val _threads = mutableMapOf<Int, ThreadModelFragment>()
+ private val _counters = mutableMapOf<String, CounterFragment>()
+
+ val threads: Collection<ThreadModelFragment> get() = _threads.values
+ val counters: Map<String, CounterFragment> get() = _counters
+
+ fun threadFor(pid: Int, name: String? = null): ThreadModelFragment {
+ var thread = _threads[pid]
+ if (thread == null) {
+ thread = ThreadModelFragment(pid, this, name)
+ _threads.put(pid, thread)
+ } else {
+ thread.hint(name = name)
+ }
+ return thread
+ }
+
+ fun addCounterSample(name: String, timestamp: Double, value: Int) {
+ _counters.getOrPut(name, { CounterFragment(name) }).events.add(timestamp hasCount value)
+ }
+
+ fun merge(other: trebuchet.model.fragments.ProcessModelFragment) {
+ if (other === this) return
+ if (id != -1 && id != other.id) {
+ throw IllegalArgumentException("Process ID mismatch")
+ }
+ hint(name = other.name)
+ other._threads.forEach { (key, value) ->
+ if (_threads.put(key, value) != null) {
+ throw IllegalStateException("Unable to merge threads of the same ID $key")
+ }
+ value.process = this
+ }
+ other._counters.forEach { (key, value) ->
+ val existing = _counters.put(key, value)
+ if (existing != null) {
+ _counters[key]!!.events.addAll(existing.events)
+ }
+ }
+ }
+
+ fun hint(id: Int = InvalidId, name: String? = null) {
+ if (this.id == InvalidId) this.id = id
+ if (this.name == null) this.name = name
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingProcessFragment.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingProcessFragment.kt
new file mode 100644
index 0000000..41adae5
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingProcessFragment.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+import trebuchet.model.CpuProcessSlice
+
+class SchedulingProcessFragment(val process: ProcessModelFragment, val thread: ThreadModelFragment, override val startTime: Double) : CpuProcessSlice {
+ override var endTime: Double = Double.MAX_VALUE
+
+ class Builder {
+ private val _slices = mutableListOf<SchedulingProcessFragment>()
+ val slices: List<SchedulingProcessFragment> get() = _slices
+ fun switchProcess(process: ProcessModelFragment, thread: ThreadModelFragment, timestamp: Double) {
+ if (_slices.isNotEmpty()) {
+ if (_slices.last().endTime == Double.MAX_VALUE) {
+ _slices.last().endTime = timestamp
+ }
+ }
+ if (thread.id != 0) {
+ _slices.add(SchedulingProcessFragment(process, thread, timestamp))
+ }
+ }
+ }
+
+ override val name: String get() {
+ if (process.name != null) {
+ return process.name!!
+ } else {
+ return process.id.toString()
+ }
+ }
+
+ override val threadName: String get() {
+ if (thread.name != null) {
+ return thread.name!!
+ }
+ return threadId.toString()
+ }
+
+ override val threadId: Int get() {
+ return thread.id
+ }
+
+ override val id: Int get() {
+ return process.id
+ }
+
+ override val didNotFinish: Boolean get() = endTime == Double.MAX_VALUE
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingSliceFragment.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingSliceFragment.kt
new file mode 100644
index 0000000..484fbe8
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/SchedulingSliceFragment.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+import trebuchet.model.SchedSlice
+import trebuchet.model.SchedulingState
+import trebuchet.model.base.Slice
+
+class SchedulingSliceFragment(override val state: SchedulingState, override val startTime: Double)
+ : SchedSlice {
+ override var endTime: Double = Double.MAX_VALUE
+ var blockedReason: String? = null
+
+ class Builder {
+ private val _slices = mutableListOf<SchedulingSliceFragment>()
+ val slices: List<SchedulingSliceFragment> get() = _slices
+
+ fun switchState(newState: SchedulingState, timestamp: Double) {
+ if (_slices.isNotEmpty()) {
+ if (_slices.last().state == newState) {
+ // Nothing to do
+ return
+ }
+ _slices.last().endTime = timestamp
+ }
+ _slices.add(SchedulingSliceFragment(newState, timestamp))
+ }
+ }
+
+ override val name: String get() = state.friendlyName
+ override val didNotFinish: Boolean get() = false
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/SliceGroupBuilder.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/SliceGroupBuilder.kt
new file mode 100644
index 0000000..b1e9750
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/SliceGroupBuilder.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+import trebuchet.model.base.SliceGroup
+
+class SliceGroupBuilder {
+ val slices: MutableList<MutableSliceGroup> = mutableListOf()
+ val openSlices: MutableList<MutableSliceGroup> = mutableListOf()
+
+ fun hasOpenSlices() = openSlices.isNotEmpty()
+
+ inline fun beginSlice(action: (MutableSliceGroup) -> Unit): Unit {
+ val builder = MutableSliceGroup()
+ action(builder)
+ openSlices.add(builder)
+ }
+
+ inline fun endSlice(action: (MutableSliceGroup) -> Unit): SliceGroup? {
+ if (!hasOpenSlices()) return null // silently ignore unmatched endSlice calls
+
+ val builder = openSlices.removeAt(openSlices.lastIndex)
+ action(builder)
+ builder.validate()
+ if (openSlices.isNotEmpty()) {
+ openSlices.last().add(builder)
+ } else {
+ slices.add(builder)
+ }
+ return builder
+ }
+
+ fun autoCloseOpenSlices(maxTimestamp: Double) {
+ while (hasOpenSlices()) {
+ endSlice {
+ it.endTime = maxTimestamp
+ it.didNotFinish = true
+ }
+ }
+ }
+
+ companion object {
+ val EmptyChildren = mutableListOf<MutableSliceGroup>()
+ }
+
+ class MutableSliceGroup(override var startTime: Double = Double.NaN,
+ override var endTime: Double = Double.NaN,
+ override var didNotFinish: Boolean = false,
+ var _name: String? = null,
+ var _children: MutableList<MutableSliceGroup>? = null) : SliceGroup {
+ override var name: String
+ get() = _name!!
+ set(value) { _name = value }
+
+ override val children: List<SliceGroup>
+ get() = _children!!
+
+ fun validate() {
+ if (!startTime.isFinite() || startTime < 0) {
+ throw IllegalStateException("Invalid startTime $startTime")
+ }
+ if (!endTime.isFinite() || endTime < 0) {
+ throw IllegalStateException("Invalid endTime $endTime")
+ }
+ if (endTime < startTime) {
+ throw IllegalStateException("endTime $endTime cannot be before startTime $startTime")
+ }
+ if (_name == null) {
+ throw IllegalStateException("name cannot be null")
+ }
+ if (_children == null) {
+ _children = EmptyChildren
+ }
+ }
+
+ fun add(child: MutableSliceGroup) {
+ if (_children == null) _children = mutableListOf()
+ _children!!.add(child)
+ }
+ }
+} \ No newline at end of file
diff --git a/core/model/src/main/kotlin/trebuchet/model/fragments/ThreadModelFragment.kt b/core/model/src/main/kotlin/trebuchet/model/fragments/ThreadModelFragment.kt
new file mode 100644
index 0000000..b4c01e1
--- /dev/null
+++ b/core/model/src/main/kotlin/trebuchet/model/fragments/ThreadModelFragment.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package trebuchet.model.fragments
+
+import trebuchet.model.InvalidId
+import trebuchet.model.SchedSlice
+import trebuchet.model.base.SliceGroup
+
+class ThreadModelFragment(var id: Int, var process: ProcessModelFragment, var name: String? = null) {
+ val slicesBuilder = SliceGroupBuilder()
+ val schedulingStateBuilder = SchedulingSliceFragment.Builder()
+
+ fun hint(pid: Int = InvalidId, name: String? = null, tgid: Int = InvalidId, processName: String? = null) {
+ if (this.id == InvalidId) this.id = pid
+ if (this.name == null) this.name = name
+ if (this.process.id == InvalidId) this.process.id = tgid
+ if (this.process.name == null) this.process.name = processName
+ }
+
+ val slices: List<SliceGroup> get() {
+ if (slicesBuilder.hasOpenSlices()) {
+ throw IllegalStateException("SliceBuilder has open slices, not finished")
+ }
+ return slicesBuilder.slices
+ }
+
+ val schedSlices: List<SchedSlice> get() {
+ // TODO: Close open slices
+ return schedulingStateBuilder.slices
+ }
+} \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..b114ed0
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+# Project-wide Gradle settings.
+org.gradle.jvmargs=-Xmx1536M
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7a3265e
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..8cb3a88
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jul 17 15:17:00 PDT 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..79b241d
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,13 @@
+This directory contains scripts for automating various tasks.
+
+## `run-startup.sh`
+
+Usage:
+```
+run-startup.sh <package name> <activity name>
+```
+
+This script automatically starts an app on a device connected through adb with
+appropriate tracing enabled. It then saves a trace and reports a summary of the
+startup behavior using StartupAnalyzerKt. This script requires `adb` to be in
+your path and that a phone be connected.
diff --git a/scripts/run-startup.sh b/scripts/run-startup.sh
new file mode 100755
index 0000000..47a4dcc
--- /dev/null
+++ b/scripts/run-startup.sh
@@ -0,0 +1,41 @@
+#!/bin/bash -x
+
+# usage: run-startup.sh <package name> <activity name>
+
+# Runs an Android app, collects a trace and prints out a summary of startup
+# metrics.
+
+PACKAGE=$1
+ACTIVITY=$2
+
+ADB=adb
+
+# Make sure we use the right adb, etc.
+$ADB root
+
+# Stop the app
+$ADB shell "am force-stop $PACKAGE"
+
+# Make sure it's compiled for speed
+$ADB shell "pm compile -m speed $PACKAGE"
+
+# Clear the page cache
+$ADB shell "echo 3 > /proc/sys/vm/drop_caches"
+
+# Start tracing
+$ADB shell "atrace -a $PACKAGE -b 32768 --async_start input dalvik view am wm sched freq idle sync irq binder_driver workq hal freq"
+
+# Launch the app
+$ADB shell "am start -W -n $PACKAGE/$ACTIVITY"
+
+# Wait a little longer for the app to do whatever it does.
+sleep 10
+
+# Capture the trace
+$ADB shell "atrace --async_stop -o /sdcard/atrace.trace"
+
+# Get the trace
+$ADB pull /sdcard/atrace.trace
+
+# Dump the startup info
+./gradlew :trebuchet:startup-analyzer:run --args="`pwd`/atrace.trace"
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..b6ea6aa
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+rootProject.name = 'TrebuchetProject'
+
+// --------- Core JVM ---------- //
+
+include 'core:model'
+include 'core:common'
+
+// -------- Main outputs ------------ //
+
+include 'trebuchet:analyzer'
+include 'trebuchet:startup-analyzer'
+include 'trebuchet:traceutils'
+include 'trebuchet:viewer'
diff --git a/trebuchet/analyzer/MANIFEST.mf b/trebuchet/analyzer/MANIFEST.mf
new file mode 100644
index 0000000..f9b7cb8
--- /dev/null
+++ b/trebuchet/analyzer/MANIFEST.mf
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: AnalyzerKt
diff --git a/trebuchet/analyzer/build.gradle b/trebuchet/analyzer/build.gradle
new file mode 100644
index 0000000..6c7c47a
--- /dev/null
+++ b/trebuchet/analyzer/build.gradle
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+apply plugin: 'java'
+apply plugin: 'application'
+apply plugin: 'kotlin-platform-jvm'
+apply plugin: 'com.github.johnrengelman.shadow'
+
+sourceCompatibility = 1.8
+mainClassName = "AnalyzerKt"
+
+sourceSets {
+ main.kotlin.srcDirs += 'src'
+}
+
+dependencies {
+ compile project(":core:common")
+ compile project(":core:model")
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+} \ No newline at end of file
diff --git a/trebuchet/analyzer/src/Analyzer.kt b/trebuchet/analyzer/src/Analyzer.kt
new file mode 100644
index 0000000..2fde368
--- /dev/null
+++ b/trebuchet/analyzer/src/Analyzer.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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 trebuchet.extras.openSample
+import trebuchet.model.Model
+import trebuchet.queries.SliceQueries
+import trebuchet.queries.ThreadQueries
+import trebuchet.util.par_map
+
+fun timeMergeShot(model: Model) {
+ println("Clock sync parent=${model.parentTimestamp}, realtime=${model.realtimeTimestamp}")
+ val slices = SliceQueries.selectAll(model) { it.name.startsWith("MergeShot")}
+ if (slices.isEmpty()) return
+ slices.forEach { println("${it.name} took ${it.duration}") }
+ val totalDuration = slices.map { it.duration }.reduce { a, b -> a+b }
+ println("Total Duration: $totalDuration")
+}
+
+
+fun measureStartup(model: Model) {
+ val uiThread = ThreadQueries.firstOrNull(model) {
+ it.slices.any {
+ it.name == "PreFork" || it.name == "activityStart"
+ }
+ } ?: return
+ val process = uiThread.process
+ val rtThread = process.threads.first { it.name == "RenderThread" }
+ val start = uiThread.slices.first {
+ it.name == "PreFork" || it.name == "activityStart"
+ }
+ val end = rtThread.slices.first {
+ it.name == "DrawFrame"
+ }
+ val startupDuration = end.endTime - start.startTime
+ println("Process ${process.name} took $startupDuration to start")
+}
+
+fun measureRotator(model: Model) {
+ val latchBuffers = SliceQueries.selectAll(model) {
+ it.name == "latchBuffer"
+ }
+ var largestDuration = 0.0
+ var latchStart = 0.0
+ var retireStart = 0.0
+ latchBuffers.forEachIndexed { index, slice ->
+ val cutoff = if (index < latchBuffers.size - 1) latchBuffers[index + 1].startTime else Double.MAX_VALUE
+ val retire = SliceQueries.selectAll(model) {
+ it.name == "sde_rotator_retire_handler"
+ && it.startTime > slice.endTime
+ && it.endTime < cutoff
+ }.firstOrNull()
+ if (retire != null) {
+ val duration = retire.endTime - slice.startTime
+ if (duration > largestDuration) {
+ largestDuration = duration
+ latchStart = slice.startTime
+ retireStart = retire.startTime
+ }
+ }
+ }
+ println("Largest duration %.2fms, occured at latchStart=%f, retireStart=%f".format(
+ largestDuration.toMilliseconds(), latchStart, retireStart))
+}
+
+private fun Double.toMilliseconds() = this / 1000.0
+
+fun main(args: Array<String>) {
+ timeMergeShot(openSample("hdr-0608-4-trace.html"))
+ //timeMergeShot(openSample("huge_trace.txt"))
+}
diff --git a/trebuchet/startup-analyzer/MANIFEST.mf b/trebuchet/startup-analyzer/MANIFEST.mf
new file mode 100644
index 0000000..a1c866b
--- /dev/null
+++ b/trebuchet/startup-analyzer/MANIFEST.mf
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: StartupAnalyzerKt
diff --git a/trebuchet/startup-analyzer/README.md b/trebuchet/startup-analyzer/README.md
new file mode 100644
index 0000000..7bab90d
--- /dev/null
+++ b/trebuchet/startup-analyzer/README.md
@@ -0,0 +1,193 @@
+# Android Application Startup Analyzer
+
+This is a tool for processing systraces and summarizing how applications spend
+their startup time. To run the tool, enter the root directory for the
+Trebuchet project and use the following command template: `./gradlew
+:trebuchet:startup-analyzer:run --args="<absolute path to trace file>"`.
+
+If you do not already have a trace file to analyze you can capture 10 seconds
+of tracing information using this command: `systrace.py -t 10 dalvik view am wm
+sched freq idle sync irq binder_driver workq hal freq`
+
+This tool is able to process either text traces from `atrace` or extract the
+trace from HTML files generated by `systrace.py`.
+
+Below is an example of its output:
+
+```
+Opening `/usr/local/google/home/chriswailes/projects/trebuchet/trace-facebook.html`
+Progress: 100.00%
+Took 428ms to import
+Parsing trace-facebook.html took 435ms
+
+App Startup summary for com.facebook.katana (29300):
+ Start offset: 1649.352 ms
+ Startup period end point: activityResume
+ Startup duration: 1033.827 ms
+ Top-level slice information:
+ activityResume
+ Event count: 1
+ Total duration: 50.860 ms
+ activityStart
+ Event count: 1
+ Total duration: 183.123 ms
+ ActivityThreadMain
+ Event count: 1
+ Total duration: 25.377 ms
+ bindApplication
+ Event count: 1
+ Total duration: 495.804 ms
+ inflate
+ Event count: 4
+ Total duration: 53.638 ms
+ Lock contention
+ Event count: 121
+ Total duration: 2.017 ms
+ PostFork
+ Event count: 1
+ Total duration: 4.041 ms
+ setCoreSettings
+ Event count: 1
+ Total duration: 0.036 ms
+ ZygoteInit
+ Event count: 1
+ Total duration: 1.895 ms
+
+ All slice information:
+ activityResume
+ Event count: 1
+ Total duration: 50.202 ms
+ activityStart
+ Event count: 1
+ Total duration: 176.956 ms
+ ActivityThreadMain
+ Event count: 1
+ Total duration: 25.377 ms
+ bindApplication
+ Event count: 1
+ Total duration: 271.592 ms
+ Collision check
+ Event count: 1
+ Total duration: 0.198 ms
+ createClassloaderNamespace
+ Event count: 1
+ Total duration: 2.119 ms
+ FrameLayout
+ Event count: 5
+ Total duration: 0.553 ms
+ FullSuspendCheck
+ Event count: 2
+ Total duration: 0.160 ms
+ inflate
+ Event count: 8
+ Total duration: 7.217 ms
+ initializeJavaContextClassLoader
+ Event count: 1
+ Total duration: 1.649 ms
+ LinearLayout
+ Event count: 3
+ Total duration: 0.501 ms
+ Load Dex files from classpath
+ Event count: 1
+ Total duration: 1.417 ms
+ Lock contention
+ Event count: 467
+ Total duration: 16.897 ms
+ makeApplication
+ Event count: 1
+ Total duration: 176.637 ms
+ monitor contention
+ Event count: 4
+ Total duration: 13.462 ms
+ NetworkSecurityConfigProvider.install
+ Event count: 1
+ Total duration: 0.253 ms
+ Obfuscated trace point
+ Event count: 20
+ Total duration: 45.005 ms
+ Open dex file
+ Event count: 16
+ Total duration: 0.880 ms
+ Event details:
+ /system/framework/org.apache.http.legacy.impl.jar @ 0.061 ms
+ /system/framework/com.google.android.maps.jar @ 0.057 ms
+ /system/framework/com.google.android.maps.jar @ 0.270 ms
+ /data/app/com.facebook.katana @ 0.040 ms
+ /data/app/com.facebook.katana @ 0.034 ms
+ /data/app/com.facebook.katana @ 0.033 ms
+ /data/app/com.facebook.katana @ 0.035 ms
+ /data/app/com.facebook.katana @ 0.034 ms
+ /data/app/com.facebook.katana @ 0.039 ms
+ /data/app/com.facebook.katana @ 0.033 ms
+ /data/app/com.facebook.katana @ 0.034 ms
+ /data/app/com.facebook.katana @ 0.033 ms
+ /data/app/com.facebook.katana @ 0.062 ms
+ /data/app/com.facebook.katana @ 0.033 ms
+ /data/app/com.facebook.katana @ 0.042 ms
+ /data/app/com.facebook.katana @ 0.040 ms
+ Open dex file function invocation
+ Event count: 16
+ Total duration: 0.153 ms
+ Open oat file
+ Event count: 8
+ Total duration: 7.566 ms
+ Event details:
+ /data/dalvik-cache/arm/system@framework@org.apache.http.legacy.impl.jar@classes.dex @ 0.028 ms
+ /system/framework/oat/arm/org.apache.http.legacy.impl.odex @ 4.073 ms
+ /data/dalvik-cache/arm/system@framework@com.google.android.maps.jar@classes.dex @ 0.026 ms
+ /system/framework/oat/arm/com.google.android.maps.odex @ 1.343 ms
+ /data/dalvik-cache/arm/data@app@com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==@base.apk@classes.dex @ 0.026 ms
+ /data/app/com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==/oat/arm/base.odex @ 2.038 ms
+ /data/dalvik-cache/arm/data@app@com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==@split_codegenerator.apk@classes.dex @ 0.017 ms
+ /data/app/com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==/oat/arm/split_codegenerator.odex @ 0.015 ms
+ OpenDexFilesFromOat
+ Event count: 4
+ Total duration: 4.261 ms
+ OpenImageFile
+ Event count: 3
+ Total duration: 0.098 ms
+ PostFork
+ Event count: 1
+ Total duration: 3.881 ms
+ RegisterDexFile
+ Event count: 4
+ Total duration: 0.660 ms
+ Event details:
+ /data/app/com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==/base.apk @ 0.482 ms
+ /data/app/com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==/base.apk!classes2.dex @ 0.061 ms
+ /data/app/com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==/base.apk!classes9.dex @ 0.054 ms
+ /data/app/com.facebook.katana-Hu0CRasfgvX30RlV_mJxBA==/base.apk!classes3.dex @ 0.063 ms
+ setCoreSettings
+ Event count: 1
+ Total duration: 0.036 ms
+ setLayerPaths
+ Event count: 1
+ Total duration: 0.035 ms
+ Setup proxies
+ Event count: 1
+ Total duration: 0.598 ms
+ setupGraphicsSupport
+ Event count: 1
+ Total duration: 1.131 ms
+ SuspendThreadByThreadId
+ Event count: 2
+ Total duration: 0.028 ms
+ Event details:
+ BgHandler id=15 @ 0.016 ms
+ P[0]_HPINeedInit2 id=21 @ 0.012 ms
+ VerifyClass
+ Event count: 2
+ Total duration: 2.405 ms
+ Event details:
+ com.facebook.common.dextricks.DexFileLoadOld @ 0.156 ms
+ com.facebook.common.dextricks.classid.ClassId @ 2.249 ms
+ View
+ Event count: 6
+ Total duration: 1.113 ms
+ ViewStub
+ Event count: 16
+ Total duration: 1.856 ms
+ ZygoteInit
+ Event count: 1
+ Total duration: 1.895 ms
+``` \ No newline at end of file
diff --git a/trebuchet/startup-analyzer/build.gradle b/trebuchet/startup-analyzer/build.gradle
new file mode 100644
index 0000000..0f3b058
--- /dev/null
+++ b/trebuchet/startup-analyzer/build.gradle
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+apply plugin: 'java'
+apply plugin: 'application'
+apply plugin: 'kotlin-platform-jvm'
+apply plugin: 'com.github.johnrengelman.shadow'
+
+sourceCompatibility = 1.8
+mainClassName = "StartupAnalyzerKt"
+
+sourceSets {
+ main.kotlin.srcDirs += 'src'
+}
+
+dependencies {
+ compile project(":core:common")
+ compile project(":core:model")
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+} \ No newline at end of file
diff --git a/trebuchet/startup-analyzer/src/StartupAnalyzer.kt b/trebuchet/startup-analyzer/src/StartupAnalyzer.kt
new file mode 100644
index 0000000..dc74f3b
--- /dev/null
+++ b/trebuchet/startup-analyzer/src/StartupAnalyzer.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/*
+ * Notes
+ *
+ * TODO (chriswailes): Support JSON output
+ * TODO (chriswailes): Move slice name parsing into slice class (if approved by Trebuchet maintainer)
+ */
+
+/*
+ * Imports
+ */
+
+import java.io.File
+import trebuchet.model.Model
+import trebuchet.extras.parseTrace
+import trebuchet.model.ThreadModel
+import trebuchet.model.base.Slice
+import trebuchet.queries.SliceQueries
+import trebuchet.queries.SliceTraverser
+import trebuchet.queries.TraverseAction
+import java.lang.Double.min
+import kotlin.system.exitProcess
+
+/*
+ * Constants
+ */
+
+// Duration (in milliseconds) used for startup period when none of the
+// appropriate events could be found.
+const val DEFAULT_START_DURATION = 5000
+
+const val PROC_NAME_SYSTEM_SERVER = "system_server"
+
+const val SLICE_NAME_ACTIVITY_DESTROY = "activityDestroy"
+const val SLICE_NAME_ACTIVITY_PAUSE = "activityPause"
+const val SLICE_NAME_ACTIVITY_RESUME = "activityResume"
+const val SLICE_NAME_ALTERNATE_DEX_OPEN_START = "Dex file open"
+const val SLICE_NAME_OPEN_DEX_FILE_FUNCTION = "std::unique_ptr<const DexFile> art::OatDexFile::OpenDexFile(std::string *) const"
+const val SLICE_NAME_OBFUSCATED_TRACE_START = "X."
+const val SLICE_NAME_PROC_START = "Start proc"
+const val SLICE_NAME_REPORT_FULLY_DRAWN = "reportFullyDrawn"
+
+val SLICE_MAPPERS = arrayOf(
+ Regex("^(Collision check)"),
+ Regex("^(Lock contention).*"),
+ Regex("^(monitor contention).*"),
+ Regex("^(NetworkSecurityConfigProvider.install)"),
+ Regex("^(Open dex file)(?: from RAM)? ([\\w\\./]*)"),
+ Regex("^(Open oat file)\\s+(.*)"),
+ Regex("^(RegisterDexFile)\\s+(.*)"),
+ Regex("^(serviceCreate):.*className=([\\w\\.]+)"),
+ Regex("^(serviceStart):.*cmp=([\\w\\./]+)"),
+ Regex("^(Setup proxies)"),
+ Regex("^(Start proc):\\s+(.*)"),
+ Regex("^(SuspendThreadByThreadId) suspended (.+)$"),
+ Regex("^(VerifyClass)(.*)"),
+
+ // Default pattern for slices with a single-word name.
+ Regex("^([\\w:]+)$")
+)
+
+/*
+ * Class Definition
+ */
+
+data class SliceInfo(val type: String, val payload: String?)
+data class StartupEndPoint(val timestamp: Double, val signal: String)
+
+/*
+ * Class Extensions
+ */
+
+private fun Double.secondValueToMillisecondString() = "%.3f ms".format(this * 1000.0)
+
+private fun <K, V> MutableMap<K, MutableList<V>>.add(key: K, el: V) {
+ this.getOrPut(key) { mutableListOf() }.add(el)
+}
+
+fun Model.findIDsByName(query_name: String): Pair<Int, Int?>? {
+ for (process in this.processes.values) {
+ if (query_name.endsWith(process.name)) {
+ return Pair(process.id, null)
+
+ } else {
+ for (thread in process.threads) {
+ if (query_name.endsWith(thread.name)) {
+ return Pair(process.id, thread.id)
+ }
+ }
+ }
+ }
+
+ return null
+}
+
+// Find the time at which we've defined startup to have ended.
+fun Model.getStartupEndTime(mainThread: ThreadModel, startupStartTime: Double): StartupEndPoint {
+ var endTimeInfo: StartupEndPoint? = null
+
+ SliceQueries.traverseSlices(mainThread, object : SliceTraverser {
+ override fun beginSlice(slice: Slice): TraverseAction {
+ if (endTimeInfo == null &&
+ (slice.name.startsWith(SLICE_NAME_ACTIVITY_DESTROY) ||
+ slice.name.startsWith(SLICE_NAME_ACTIVITY_PAUSE) ||
+ slice.name.startsWith(SLICE_NAME_ACTIVITY_RESUME))) {
+
+ endTimeInfo = StartupEndPoint(slice.endTime, slice.name)
+
+ return TraverseAction.VISIT_CHILDREN
+ } else if (slice.name.startsWith(SLICE_NAME_REPORT_FULLY_DRAWN)) {
+ endTimeInfo = StartupEndPoint(slice.endTime, slice.name)
+
+ return TraverseAction.DONE
+ } else {
+ return TraverseAction.VISIT_CHILDREN
+ }
+ }
+ })
+
+ return endTimeInfo
+ ?: StartupEndPoint(min(startupStartTime + DEFAULT_START_DURATION, this.endTimestamp), "DefaultDuration")
+}
+
+/*
+ * Helper Functions
+ */
+
+fun parseSliceInfo(sliceString: String): SliceInfo {
+ when {
+ // Handle special cases.
+ sliceString == SLICE_NAME_OPEN_DEX_FILE_FUNCTION -> return SliceInfo("Open dex file function invocation", null)
+ sliceString.startsWith(SLICE_NAME_ALTERNATE_DEX_OPEN_START) -> return SliceInfo("Open dex file", sliceString.split(" ").last().trim())
+ sliceString.startsWith(SLICE_NAME_OBFUSCATED_TRACE_START) -> return SliceInfo("Obfuscated trace point", null)
+ sliceString[0] == '/' -> return SliceInfo("Load Dex files from classpath", null)
+
+ else -> {
+ // Search the slice mapping patterns.
+ for (pattern in SLICE_MAPPERS) {
+ val matchResult = pattern.find(sliceString)
+
+ if (matchResult != null) {
+ val sliceType = matchResult.groups[1]!!.value.trim()
+
+ val sliceDetails =
+ if (matchResult.groups.size > 2 && !matchResult.groups[2]!!.value.isEmpty()) {
+ matchResult.groups[2]!!.value.trim()
+ } else {
+ null
+ }
+
+ return SliceInfo(sliceType, sliceDetails)
+ }
+ }
+
+ return SliceInfo("Unknown Slice", sliceString)
+ }
+ }
+
+}
+
+fun measureStartup(model: Model) {
+ val systemServerIDs = model.findIDsByName(PROC_NAME_SYSTEM_SERVER)
+
+ if (systemServerIDs == null) {
+ println("Error: Unable to find the System Server")
+ exitProcess(1)
+ }
+
+ val systemServerProc = model.processes[systemServerIDs.first]
+ val startedApps: MutableMap<Int, Pair<String, Double>> = mutableMapOf()
+
+ systemServerProc!!.threads.forEach { thread ->
+ SliceQueries.iterSlices(thread) { slice ->
+ if (slice.name.startsWith(SLICE_NAME_PROC_START)) {
+
+ val newProcName = slice.name.split(':', limit = 2)[1].trim()
+ val newProcIDs = model.findIDsByName(newProcName)
+
+ when {
+ newProcIDs == null -> println("Unable to find PID for process `$newProcName`")
+ startedApps.containsKey(newProcIDs.first) -> println("PID already mapped to started app")
+ else -> startedApps[newProcIDs.first] = Pair(newProcName, slice.startTime)
+ }
+ }
+ }
+ }
+
+ startedApps.forEach { pid, (startedName, startupStartTime) ->
+ val process = model.processes[pid]
+ val mainThread = process!!.threads[0]
+
+ val startupEndTime = model.getStartupEndTime(mainThread, startupStartTime)
+ var unallocatedTime = 0.0
+
+ val topLevelSlices = mutableMapOf<String, MutableList<Double>>()
+
+ // Triple := Duration, Duration Self, Payload
+ val allSlices = mutableMapOf<String, MutableList<Triple<Double, Double, String?>>>()
+
+ SliceQueries.traverseSlices(mainThread, object : SliceTraverser {
+ // Our depth down an individual tree in the slice forest.
+ var treeDepth = -1
+
+ var lastTopLevelSlice : Slice? = null
+
+ override fun beginSlice(slice: Slice): TraverseAction {
+ ++this.treeDepth
+
+ if (slice.startTime < startupEndTime.timestamp) {
+ // This slice starts during the startup period. If it
+ // ends within the startup period we will record info
+ // from this slice. Either way we will visit its
+ // children.
+
+ if (this.treeDepth == 0 && this.lastTopLevelSlice != null) {
+ unallocatedTime += (slice.startTime - this.lastTopLevelSlice!!.endTime)
+ }
+
+ if (slice.endTime <= startupEndTime.timestamp) {
+
+ val sliceInfo = parseSliceInfo(slice.name)
+
+ allSlices.add(sliceInfo.type, Triple(slice.duration, slice.durationSelf, sliceInfo.payload))
+ if (this.treeDepth == 0) topLevelSlices.add(sliceInfo.type, slice.duration)
+ }
+
+ return TraverseAction.VISIT_CHILDREN
+
+ } else {
+ // All contents of this slice occur after the startup
+ // period has ended. We don't need to record anything
+ // or traverse any children.
+ return TraverseAction.DONE
+ }
+ }
+
+ override fun endSlice(slice: Slice) {
+ if (this.treeDepth == 0) {
+ lastTopLevelSlice = slice
+ }
+
+ --this.treeDepth
+ }
+ })
+
+ println()
+ println("App Startup summary for $startedName (${process.id}):")
+ println("\tStart offset: ${(startupStartTime - model.beginTimestamp).secondValueToMillisecondString()}")
+ println("\tStartup period end point: ${startupEndTime.signal}")
+ println("\tTime to first slice: ${(mainThread.slices.first().startTime - startupStartTime).secondValueToMillisecondString()}")
+ println("\tStartup duration: ${(startupEndTime.timestamp - startupStartTime).secondValueToMillisecondString()}")
+ println("\tUnallocated time: ${unallocatedTime.secondValueToMillisecondString()}")
+ println("\tTop-level slice information:")
+ topLevelSlices.toSortedMap(java.lang.String.CASE_INSENSITIVE_ORDER).forEach { sliceType, eventDurations ->
+ println("\t\t$sliceType")
+ println("\t\t\tEvent count: ${eventDurations.count()}")
+ println("\t\t\tTotal duration: ${eventDurations.sum().secondValueToMillisecondString()}")
+ }
+ println()
+ println("\tAll slice information:")
+ allSlices.toSortedMap(java.lang.String.CASE_INSENSITIVE_ORDER).forEach { sliceType, eventList ->
+ println("\t\t$sliceType")
+ println("\t\t\tEvent count: ${eventList.count()}")
+
+ var totalDuration = 0.0
+ var totalDurationSelf = 0.0
+ var numSliceDetails = 0
+
+ eventList.forEach { (duration, durationSelf, details) ->
+ totalDuration += duration
+ totalDurationSelf += durationSelf
+ if (details != null) ++numSliceDetails
+ }
+
+ println("\t\t\tTotal duration: ${totalDuration.secondValueToMillisecondString()}")
+ println("\t\t\tTotal duration (self): ${totalDurationSelf.secondValueToMillisecondString()}")
+
+ if (numSliceDetails > 0) {
+ println("\t\t\tEvent details:")
+ eventList.forEach { (duration, durationSelf, details) ->
+ if (details != null) println("\t\t\t\t$details @ ${duration.secondValueToMillisecondString()}")
+ }
+ }
+ }
+ println()
+ }
+}
+
+/*
+ * Main Function
+ */
+
+fun main(args: Array<String>) {
+ if (args.isEmpty()) {
+ println("Usage: StartAnalyzerKt <trace filename>")
+ return
+ }
+
+ val filename = args[0]
+
+ println("Opening `$filename`")
+ val trace = parseTrace(File(filename))
+ measureStartup(trace)
+} \ No newline at end of file
diff --git a/trebuchet/traceutils/MANIFEST.mf b/trebuchet/traceutils/MANIFEST.mf
new file mode 100644
index 0000000..f3980c7
--- /dev/null
+++ b/trebuchet/traceutils/MANIFEST.mf
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: TraceUtilsKt
diff --git a/trebuchet/traceutils/build.gradle b/trebuchet/traceutils/build.gradle
new file mode 100644
index 0000000..d69945f
--- /dev/null
+++ b/trebuchet/traceutils/build.gradle
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+apply plugin: 'java'
+apply plugin: 'application'
+apply plugin: 'kotlin-platform-jvm'
+apply plugin: 'com.github.johnrengelman.shadow'
+
+sourceCompatibility = 1.8
+mainClassName = "TraceUtilsKt"
+
+sourceSets {
+ main.kotlin.srcDirs += 'src'
+}
+
+dependencies {
+ compile project(":core:common")
+ compile project(":core:model")
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+} \ No newline at end of file
diff --git a/trebuchet/traceutils/src/TraceUtils.kt b/trebuchet/traceutils/src/TraceUtils.kt
new file mode 100644
index 0000000..6f2bd76
--- /dev/null
+++ b/trebuchet/traceutils/src/TraceUtils.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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 trebuchet.extractors.ExtractorRegistry
+import trebuchet.extras.InputStreamAdapter
+import trebuchet.extras.findSampleData
+import trebuchet.importers.ImporterRegistry
+import trebuchet.io.BufferProducer
+import trebuchet.io.StreamingReader
+import trebuchet.util.PrintlnImportFeedback
+import java.io.File
+import java.io.OutputStream
+
+fun validateSrcDest(source: File, destDir: File) {
+ if (!source.exists()) {
+ throw IllegalArgumentException("No such file '$source'")
+ }
+ if (!destDir.exists()) {
+ destDir.mkdirs()
+ if (!destDir.exists() && destDir.isDirectory) {
+ throw IllegalArgumentException("'$destDir' isn't a directory")
+ }
+ }
+}
+
+fun findTraces(stream: BufferProducer,
+ onlyKnownFormats: Boolean,
+ traceFound: (StreamingReader) -> Unit) {
+ val importFeedback = PrintlnImportFeedback()
+ try {
+ val reader = StreamingReader(stream)
+ reader.loadIndex(reader.keepLoadedSize.toLong())
+ val extractor = ExtractorRegistry.extractorFor(reader, importFeedback)
+ if (extractor != null) {
+ extractor.extract(reader, {
+ subStream -> findTraces(subStream, onlyKnownFormats, traceFound)
+ })
+ } else {
+ if (onlyKnownFormats) {
+ val importer = ImporterRegistry.importerFor(reader, importFeedback)
+ if (importer != null) {
+ traceFound(reader)
+ }
+ } else {
+ traceFound(reader)
+ }
+ }
+ } catch (ex: Throwable) {
+ importFeedback.reportImportException(ex)
+ } finally {
+ stream.close()
+ }
+}
+
+fun copy(reader: StreamingReader, output: OutputStream) {
+ reader.windows.forEach {
+ val slice = it.slice
+ output.write(slice.buffer, slice.startIndex, slice.length)
+ }
+ var next = reader.source.next()
+ while (next != null) {
+ output.write(next.buffer, next.startIndex, next.length)
+ next = reader.source.next()
+ }
+}
+
+fun extract(source: File, destDir: File) {
+ validateSrcDest(source, destDir)
+ println("Extracting ${source.name} to ${destDir.path}")
+ var traceCount = 0
+ findTraces(InputStreamAdapter(source), onlyKnownFormats = false) {
+ trace ->
+ val destFile = File(destDir, "${source.nameWithoutExtension}_$traceCount")
+ println("Found subtrace, extracting to $destFile")
+ val outputStream = destFile.outputStream()
+ copy(trace, outputStream)
+ traceCount++
+ }
+
+}
+
+fun main(args: Array<String>) {
+ if (args.size != 2) {
+ println("Usage: <source file> <dest dir>")
+ return
+ }
+ try {
+ val source = File(args[0])
+ val destDir = File(args[1])
+ extract(source, destDir)
+ } catch (ex: Exception) {
+ println(ex.message)
+ }
+}
diff --git a/trebuchet/viewer/MANIFEST.mf b/trebuchet/viewer/MANIFEST.mf
new file mode 100644
index 0000000..408672f
--- /dev/null
+++ b/trebuchet/viewer/MANIFEST.mf
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: ViewerKt
diff --git a/trebuchet/viewer/build.gradle b/trebuchet/viewer/build.gradle
new file mode 100644
index 0000000..69628e9
--- /dev/null
+++ b/trebuchet/viewer/build.gradle
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+apply plugin: 'java'
+apply plugin: 'application'
+apply plugin: 'kotlin-platform-jvm'
+
+sourceCompatibility = 1.8
+mainClassName = "ViewerKt"
+
+dependencies {
+ compile project(":core:common")
+ compile project(":core:model")
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/Animator.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/Animator.kt
new file mode 100644
index 0000000..97a6881
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/Animator.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import java.awt.event.ActionListener
+import javax.swing.Timer
+
+fun animate(start: Double, end: Double, setter: (Double) -> Unit) {
+ AnimationPulse.register(Animator(start, end, setter))
+}
+
+class Animator(val start: Double, val end: Double, val setter: (Double) -> Unit, val duration: Int = 150) {
+ val startTime: Long = System.currentTimeMillis()
+
+ fun animate(now: Long): Boolean {
+ val frac = maxOf(0f, minOf(1f, (now - startTime).toFloat() / duration.toFloat()))
+ val value: Double = ((end - start) * frac) + start
+ setter(value)
+ return frac >= 1f
+ }
+}
+
+object AnimationPulse {
+ private val animators = mutableListOf<Animator>()
+
+ fun register(animator: Animator) {
+ animators.add(animator)
+ if (animators.size == 1) {
+ animationTimer.restart()
+ }
+ }
+
+ private val animationPulse: ActionListener = ActionListener {
+ val now = System.currentTimeMillis()
+ animators.removeIf { it.animate(now) }
+ if (animators.isNotEmpty()) {
+ animationTimer.restart()
+ }
+ }
+
+ private val animationTimer = Timer(15, animationPulse)
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/LayoutConstants.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/LayoutConstants.kt
new file mode 100644
index 0000000..99eb02b
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/LayoutConstants.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+object LayoutConstants {
+ const val TrackHeight = 20
+ const val SchedTrackHeight = 5
+ const val RowLabelWidth = 200
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/Os.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/Os.kt
new file mode 100644
index 0000000..e170606
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/Os.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import javax.swing.UIManager
+
+object Os {
+ fun main() {
+ System.setProperty("apple.laf.useScreenMenuBar", "true")
+ }
+
+ fun swingInit() {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessLabel.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessLabel.kt
new file mode 100644
index 0000000..2a406c8
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessLabel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import javax.swing.BorderFactory
+import javax.swing.JLabel
+
+class ProcessLabel(text: String) : JLabel(text) {
+ init {
+ background = ThemeColors.ProcessLabelBackground
+ foreground = ThemeColors.ProcessLabelText
+ isOpaque = true
+ border = BorderFactory.createEmptyBorder(2, 4, 2, 0)
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessPanel.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessPanel.kt
new file mode 100644
index 0000000..1f5f1d9
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ProcessPanel.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import traceviewer.ui.tracks.MultiLineTrack
+import trebuchet.model.ProcessModel
+import java.awt.Dimension
+import java.awt.GridBagConstraints
+import java.awt.GridBagLayout
+import javax.swing.JPanel
+
+class ProcessPanel(val process: ProcessModel, renderState: RenderState) : JPanel(GridBagLayout()) {
+ init {
+ val constraints = GridBagConstraints()
+ constraints.gridwidth = GridBagConstraints.REMAINDER
+ constraints.anchor = GridBagConstraints.LINE_START
+ constraints.fill = GridBagConstraints.HORIZONTAL
+ add(ProcessLabel(process.name), constraints)
+ process.threads.forEach {
+ constraints.gridwidth = 1
+ constraints.weightx = 0.0
+ constraints.anchor = GridBagConstraints.PAGE_START
+ add(RowLabel(it.name), constraints)
+ constraints.weightx = 1.0
+ constraints.gridwidth = GridBagConstraints.REMAINDER
+ add(MultiLineTrack(it, renderState), constraints)
+ }
+ maximumSize = Dimension(Int.MAX_VALUE, preferredSize.height)
+ isOpaque = true
+ background = ThemeColors.RowLabelBackground
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/RenderState.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/RenderState.kt
new file mode 100644
index 0000000..198e7b5
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/RenderState.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import java.awt.event.ActionListener
+import javax.swing.Timer
+
+class RenderState(minX: Double, maxX: Double, viewWidth: Int) {
+ var scale: Double
+ private set
+ var panX: Double
+ private set
+
+ val listeners = mutableListOf<() -> Unit>()
+
+ init {
+ panX = minX
+ scale = viewWidth / (maxX - minX)
+ }
+
+ private fun notifyListeners() {
+ listeners.forEach { it() }
+ }
+
+ fun xViewToWorld(x: Int) = (x / scale) + panX
+
+ fun zoomBy(amount: Double, viewX: Int) {
+ val worldX = xViewToWorld(viewX)
+ val endScale = scale * amount
+ animate(scale, endScale) {
+ scale = it
+ panX = worldX - (viewX / scale)
+ notifyListeners()
+ }
+ }
+
+ fun pan(deltaX: Int) {
+ val endPanX = panX + (deltaX / scale)
+ animate(panX, endPanX) { value ->
+ panX = value
+ notifyListeners()
+ }
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/RowLabel.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/RowLabel.kt
new file mode 100644
index 0000000..fb7462e
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/RowLabel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import java.awt.Dimension
+import javax.swing.BorderFactory
+import javax.swing.JLabel
+
+class RowLabel constructor(str: String) : JLabel(str) {
+ init {
+ border = BorderFactory.createEmptyBorder(2, 8, 2, 0)
+ foreground = ThemeColors.RowLabelText
+ }
+ override fun getPreferredSize(): Dimension {
+ val dimen = super.getPreferredSize()
+ dimen.width = LayoutConstants.RowLabelWidth
+ return dimen
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ThemeColors.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ThemeColors.kt
new file mode 100644
index 0000000..b07ad36
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/ThemeColors.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import java.awt.Color
+
+object ThemeColors {
+ val SliceTrackBackground = Color.decode("#2b2b2b")
+
+ val RowLabelBackground = Color.decode("#313335")
+ val RowLabelText = Color.decode("#9A9DA0")
+
+ val ProcessLabelBackground = Color.decode("#3c3f41")
+ val ProcessLabelText = Color.decode("#A9B7C6")
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/TimelineView.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/TimelineView.kt
new file mode 100644
index 0000000..1b291ab
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/TimelineView.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import trebuchet.model.Model
+import java.awt.Dimension
+import java.awt.Point
+import java.awt.Rectangle
+import java.awt.event.*
+import javax.swing.*
+
+
+class TimelineView(val model: Model, val renderState: RenderState) : JScrollPane(), MouseListener,
+ MouseMotionListener {
+
+ private val lastMousePosition = Point()
+
+ init {
+ val box = object : JPanel(), Scrollable {
+
+ override fun getScrollableTracksViewportWidth(): Boolean {
+ return true
+ }
+
+ override fun getScrollableTracksViewportHeight(): Boolean {
+ return false
+ }
+
+ override fun getScrollableBlockIncrement(visibleRect: Rectangle, orientation: Int,
+ direction: Int): Int {
+ return visibleRect.height - LayoutConstants.TrackHeight
+ }
+
+ override fun getScrollableUnitIncrement(visibleRect: Rectangle, orientation: Int,
+ direction: Int): Int {
+ return LayoutConstants.TrackHeight
+ }
+
+ override fun getPreferredScrollableViewportSize(): Dimension {
+ return preferredSize
+ }
+
+ init {
+ layout = BoxLayout(this, BoxLayout.PAGE_AXIS)
+ }
+ }
+ model.processes.values.filter { it.hasContent }.forEach {
+ box.add(ProcessPanel(it, renderState))
+ }
+ setViewportView(box)
+ addMouseListener(this)
+ addMouseMotionListener(this)
+ lastMousePosition.setLocation(width / 2, 0)
+ renderState.listeners.add({ repaint() })
+ // TODO: Add workaround for OSX 'defaults write -g ApplePressAndHoldEnabled -bool false'
+ addAction('w', "zoomIn") {
+ renderState.zoomBy(1.5, zoomFocalPointX)
+ }
+ addAction('s', "zoomOut") {
+ renderState.zoomBy(1 / 1.5, zoomFocalPointX)
+ }
+ addAction('a', "panLeft") {
+ renderState.pan((width * -.3).toInt())
+ }
+ addAction('d', "panRight") {
+ renderState.pan((width * .3).toInt())
+ }
+ }
+
+ fun addAction(key: Char, name: String, listener: (ActionEvent) -> Unit) {
+ inputMap.put(KeyStroke.getKeyStroke(key), name)
+ actionMap.put(name, object : AbstractAction(name) {
+ override fun actionPerformed(event: ActionEvent) {
+ listener.invoke(event)
+ }
+ })
+ }
+
+ override fun isFocusable() = true
+
+ private val zoomFocalPointX: Int get() {
+ return maxOf(0, minOf(this.width, lastMousePosition.x - LayoutConstants.RowLabelWidth))
+ }
+
+ override fun mouseReleased(e: MouseEvent) {
+
+ }
+
+ override fun mouseEntered(e: MouseEvent) {
+
+ }
+
+ override fun mouseClicked(e: MouseEvent) {
+
+ }
+
+ override fun mouseExited(e: MouseEvent) {
+
+ }
+
+ override fun mousePressed(e: MouseEvent) {
+
+ }
+
+ override fun mouseMoved(e: MouseEvent) {
+ lastMousePosition.location = e.point
+ }
+
+ override fun mouseDragged(e: MouseEvent) {
+
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/TraceViewerWindow.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/TraceViewerWindow.kt
new file mode 100644
index 0000000..f95ab13
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/TraceViewerWindow.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui
+
+import traceviewer.ui.builders.MenuBar
+import traceviewer.ui.components.ChooseTrace
+import trebuchet.io.BufferProducer
+import trebuchet.io.DataSlice
+import trebuchet.io.asSlice
+import trebuchet.model.Model
+import trebuchet.task.ImportTask
+import trebuchet.util.PrintlnImportFeedback
+import java.awt.Dimension
+import java.io.BufferedInputStream
+import java.io.File
+import java.io.FileInputStream
+import javax.swing.*
+
+
+class TraceViewerWindow {
+
+ private var _model: Model? = null
+ val frame = JFrame("TraceViewer")
+
+ init {
+ frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
+ frame.size = Dimension(1400, 1000)
+ frame.jMenuBar = createMenuBar()
+ frame.contentPane = ChooseTrace { file -> import(file) }
+ frame.isLocationByPlatform = true
+ frame.isVisible = true
+ }
+
+ constructor(file: File) {
+ import(file)
+ }
+
+ private fun createMenuBar(): JMenuBar {
+ return MenuBar {
+ menu("File") {
+ item("Open") {
+ action { ChooseTrace.selectFile(frame.location) { import(it) } }
+ }
+ }
+ }
+ }
+
+ fun import(file: File) {
+ object : SwingWorker<Model, Void>() {
+ override fun doInBackground(): Model {
+ val progress = ProgressMonitorInputStream(frame, "Importing ${file.name}",
+ FileInputStream(file))
+ progress.progressMonitor.millisToDecideToPopup = 0
+ progress.progressMonitor.millisToPopup = 0
+ val stream = BufferedInputStream(progress)
+
+ val task = ImportTask(PrintlnImportFeedback())
+ val model = task.import(object : BufferProducer {
+ override fun next(): DataSlice? {
+ val buffer = ByteArray(2 * 1024 * 1024)
+ val read = stream.read(buffer)
+ if (read == -1) return null
+ return buffer.asSlice(read)
+ }
+
+ override fun close() {
+ stream.close()
+ }
+ })
+ println("Import finished")
+ return model
+ }
+
+ override fun done() {
+ println("Done called")
+ model = get()
+ }
+ }.execute()
+ }
+
+ var model: Model?
+ get() = _model
+ set(model) {
+ _model = model
+ if (model != null) {
+ val boost = model.duration * .15
+ val zoom = RenderState(model.beginTimestamp - boost,
+ model.endTimestamp + boost,
+ frame.size.width - LayoutConstants.RowLabelWidth)
+ val timelineView = TimelineView(model, zoom)
+ frame.contentPane = timelineView
+ timelineView.requestFocus()
+ frame.revalidate()
+ frame.repaint()
+ }
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/builders/Menu.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/builders/Menu.kt
new file mode 100644
index 0000000..1f92ade
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/builders/Menu.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui.builders
+
+import java.awt.event.ActionEvent
+import javax.swing.JMenu
+import javax.swing.JMenuBar
+import javax.swing.JMenuItem
+
+class MenuBar private constructor(val base: JMenuBar) {
+ companion object {
+ operator fun invoke(init: MenuBar.() -> Unit): JMenuBar {
+ val base = JMenuBar()
+ MenuBar(base).init()
+ return base
+ }
+ }
+
+ fun menu(name: String, init: Menu.() -> Unit) {
+ base.add(Menu(name, init))
+ }
+}
+
+class Menu private constructor(val base: JMenu) {
+ companion object {
+ operator fun invoke(name: String, init: Menu.() -> Unit): JMenu {
+ val base = JMenu(name)
+ Menu(base).init()
+ return base
+ }
+ }
+
+ fun item(name: String, init: MenuItem.() -> Unit) {
+ base.add(MenuItem(name, init))
+ }
+}
+
+class MenuItem private constructor(val base: JMenuItem) {
+ companion object {
+ operator fun invoke(name: String, init: MenuItem.() -> Unit): JMenuItem {
+ val base = JMenuItem(name)
+ MenuItem(base).init()
+ return base
+ }
+ }
+
+ fun action(listener: (ActionEvent) -> Unit) {
+ base.addActionListener(listener)
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/components/ChooseTrace.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/components/ChooseTrace.kt
new file mode 100644
index 0000000..9c66303
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/components/ChooseTrace.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui.components
+
+import java.awt.FileDialog
+import java.awt.Frame
+import java.awt.Point
+import java.io.File
+import javax.swing.JLabel
+import javax.swing.JPanel
+
+class ChooseTrace(val listener: (File) -> Unit) : JPanel() {
+ companion object {
+ fun selectFile(location: Point, fileSelected: (File) -> Unit) {
+ val fd = FileDialog(null as Frame?)
+ fd.setLocation(location.x + 100, location.y + 50)
+ fd.isVisible = true
+ val filename = fd.file
+ if (filename != null) {
+ val file = File(fd.directory, filename)
+ if (file.exists()) {
+ fileSelected(file)
+ }
+ }
+ }
+ }
+
+ init {
+ add(JLabel("No file selected"))
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/MultiLineTrack.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/MultiLineTrack.kt
new file mode 100644
index 0000000..6982e19
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/MultiLineTrack.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui.tracks
+
+import traceviewer.ui.ThemeColors
+import traceviewer.ui.RenderState
+import trebuchet.model.ThreadModel
+import trebuchet.model.base.SliceGroup
+import javax.swing.BorderFactory
+import javax.swing.BoxLayout
+import javax.swing.JPanel
+
+class MultiLineTrack(thread: ThreadModel, renderState: RenderState) : JPanel() {
+ init {
+ isOpaque = true
+ layout = BoxLayout(this, BoxLayout.PAGE_AXIS)
+ border = BorderFactory.createEmptyBorder(4, 0, 0, 0)
+ background = ThemeColors.SliceTrackBackground
+ if (thread.schedSlices.isNotEmpty()) {
+ add(SchedTrack(thread.schedSlices, renderState))
+ }
+ val rows = RowCreater(thread.slices).rows
+ rows.forEach {
+ add(SliceTrack(it, renderState))
+ }
+ }
+
+ class RowCreater constructor(slices: List<SliceGroup>) {
+ val rows = mutableListOf<MutableList<SliceGroup>>()
+ var index = -1
+
+ init {
+ push()
+ slices.forEach { addSlice(it) }
+ }
+
+ val row: MutableList<SliceGroup> get() = rows[index]
+
+ fun push() {
+ index++
+ while (index >= rows.size) rows.add(mutableListOf())
+ }
+
+ fun pop() {
+ index--
+ }
+
+ fun addSlice(slice: SliceGroup) {
+ row.add(slice)
+ if (slice.children.isNotEmpty()) {
+ push()
+ slice.children.forEach { addSlice(it) }
+ pop()
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SchedTrack.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SchedTrack.kt
new file mode 100644
index 0000000..812acc4
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SchedTrack.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui.tracks
+
+import traceviewer.ui.LayoutConstants
+import traceviewer.ui.RenderState
+import trebuchet.model.SchedSlice
+import trebuchet.model.SchedulingState
+import java.awt.Color
+import java.awt.FontMetrics
+import java.awt.Graphics
+
+class SchedTrack(slices: List<SchedSlice>, renderState: RenderState)
+ : SliceTrack<SchedSlice>(slices, renderState, trackHeight = LayoutConstants.SchedTrackHeight) {
+
+ override fun colorFor(slice: SchedSlice): Color {
+ return when (slice.state) {
+ SchedulingState.WAKING,
+ SchedulingState.RUNNABLE -> Runnable
+ SchedulingState.RUNNING -> Running
+ SchedulingState.SLEEPING -> Sleeping
+ else -> UnintrSleep
+ }
+ }
+
+ override fun drawLabel(slice: SchedSlice, g: Graphics, metrics: FontMetrics, x: Int, y: Int, width: Int) {
+ }
+
+ companion object Colors {
+ val Runnable = Color.decode("#03A9F4")
+ val Running = Color.decode("#4CAF50")
+ val Sleeping = Color.decode("#424242")
+ val UnintrSleep = Color.decode("#A71C1C")
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SliceTrack.kt b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SliceTrack.kt
new file mode 100644
index 0000000..9f9fc13
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/traceviewer/ui/tracks/SliceTrack.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package traceviewer.ui.tracks
+
+import traceviewer.ui.LayoutConstants
+import traceviewer.ui.RenderState
+import trebuchet.model.base.Slice
+import java.awt.Color
+import java.awt.Dimension
+import java.awt.FontMetrics
+import java.awt.Graphics
+import javax.swing.JComponent
+
+open class SliceTrack<in T : Slice>(private val slices: List<T>, private val renderState: RenderState,
+ trackHeight: Int = LayoutConstants.TrackHeight) : JComponent() {
+
+ init {
+ preferredSize = Dimension(0, trackHeight)
+ size = preferredSize
+ }
+
+ override fun paintComponent(g: Graphics?) {
+ if (g == null) return
+
+ val scale = renderState.scale
+ val panX = renderState.panX
+ val y = 0
+ val height = height
+ var x: Int
+ var width: Int
+
+ val metrics = g.fontMetrics
+ var ty = metrics.ascent
+ ty += (height - ty) / 2
+
+ slices.forEach {
+ x = ((it.startTime - panX) * scale).toInt()
+ val scaledWidth = (it.endTime - it.startTime) * scale
+ width = maxOf(scaledWidth.toInt(), 1)
+ if (x + width > 0 && x < this.width) {
+ var color = colorFor(it)
+ if (scaledWidth < 1) {
+ color = Color(color.red, color.green, color.blue,
+ maxOf((255 * scaledWidth).toInt(), 50))
+ }
+ g.color = color
+ g.fillRect(x, y, width, height)
+
+ if (height >= metrics.height) {
+ drawLabel(it, g, metrics, x, ty, width)
+ }
+ }
+ }
+ }
+
+ open fun drawLabel(slice: T, g: Graphics, metrics: FontMetrics, x: Int, y: Int, width: Int) {
+ var strLimit = 0
+ var strWidth = 0
+ while (strLimit < slice.name.length && strWidth <= width) {
+ strWidth += metrics.charWidth(slice.name[strLimit])
+ strLimit++
+ }
+ if (strWidth > width) strLimit--
+ if (strLimit > 2) {
+ g.color = textColor
+ g.drawString(slice.name.substring(0, strLimit), x, y)
+ }
+ }
+
+ open fun colorFor(slice: T): Color {
+ return colors[Math.abs(slice.name.hashCode()) % colors.size]
+ }
+
+ private fun sliceAt(timestamp: Double) = slices.binarySearch {
+ when {
+ it.startTime > timestamp -> 1
+ it.endTime < timestamp -> -1
+ else -> 0
+ }
+ }.let { if (it < 0) null else slices[it] }
+
+ companion object Style {
+ private val colors = listOf(
+ Color(0x0D47A1), Color(0x1565C0), Color(0x1976D2),
+ Color(0x1A237E), Color(0x283593), Color(0x303F9F),
+ Color(0x01579B), Color(0x006064), Color(0x004D40),
+ Color(0x1B5E20), Color(0x37474F), Color(0x263238)
+ )
+
+ private val textColor = Color(0xff, 0xff, 0xff, 0x7f)
+ }
+} \ No newline at end of file
diff --git a/trebuchet/viewer/src/main/kotlin/viewer.kt b/trebuchet/viewer/src/main/kotlin/viewer.kt
new file mode 100644
index 0000000..76e999a
--- /dev/null
+++ b/trebuchet/viewer/src/main/kotlin/viewer.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * 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
+ *
+ * https://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 traceviewer.ui.Os
+import traceviewer.ui.TraceViewerWindow
+import trebuchet.extras.findSampleData
+import java.io.File
+import javax.swing.SwingUtilities
+
+fun main(args: Array<String>) {
+ Os.main()
+ SwingUtilities.invokeLater {
+ Os.swingInit()
+ TraceViewerWindow(File(findSampleData(), "sample.ftrace"))
+ }
+} \ No newline at end of file