diff options
| author | John Reck <jreck@google.com> | 2018-10-08 20:32:38 -0700 |
|---|---|---|
| committer | android-build-merger <android-build-merger@google.com> | 2018-10-08 20:32:38 -0700 |
| commit | d623feb17369a7dc58dcdde927cbd49ecafbb5b3 (patch) | |
| tree | 49ee5735654d00a3ff7fa43a1fd460580f5b52fb | |
| parent | 7f0a7b9711a6bbc8f5277908cbbc4c25814c7026 (diff) | |
| parent | 842adf2777b457b504a1239963bce504fd70f5eb (diff) | |
| download | platform_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
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 @@ -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 Binary files differnew file mode 100644 index 0000000..7a3265e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar 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 @@ -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 |
