diff options
author | Oleg Shaldybin <olegsh@google.com> | 2023-06-16 02:02:13 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-06-16 02:02:13 +0000 |
commit | 1488d1d1de4862d56164641ff2db3d9bed345cb6 (patch) | |
tree | 248e54a31421fdcdebfaf7e83aace32f7bc8a8ad | |
parent | 68ac043b69bd19a8c64348357861b1df81c627b3 (diff) | |
parent | 4a4b713b448fa7874945e616ea82dc77062cdd78 (diff) | |
download | perfmark-1488d1d1de4862d56164641ff2db3d9bed345cb6.tar.gz |
Import perfmark v-0.26 am: 9d6c9c0f44 am: a14e499d34 am: e0ee6365ff am: 4a4b713b44
Original change: https://android-review.googlesource.com/c/platform/external/perfmark/+/2627042
Change-Id: I67444728877d1d6f1062b5453c0211bc53912749
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
143 files changed, 13897 insertions, 0 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1ef0730 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 0000000..497d1f3 --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,10 @@ +name: "Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..ecbdc41 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,27 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f80a97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Gradle +build +gradle.properties +.gradle +local.properties +out + +# Maven (examples) +target + +# Bazel +bazel-bin +bazel-examples +bazel-genfiles +bazel-perfmark +bazel-out +bazel-testlogs + +# IntelliJ IDEA +.idea +*.iml + +# Eclipse +.classpath +.project +.settings +.gitignore +bin + +# OS X +.DS_Store + +# Emacs +*~ +\#*\# diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..32df005 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +## 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. + @@ -0,0 +1,201 @@ + 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. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..085e623 --- /dev/null +++ b/METADATA @@ -0,0 +1,17 @@ +name: "Perfmark" +description: + "PerfMark is a High Performance Tracing Library" + +third_party { + url { + type: HOMEPAGE + value: "http://www.perfmark.io" + } + url { + type: GIT + value: "https://github.com/perfmark/perfmark.git" + } + version: "0.26.0" + last_upgrade_date { year: 2023 month: 6 day: 2 } + license_type: PERMISSIVE +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1,41 @@ + +Copyright 2019 Google LLC + +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. + +----------------------------------------------------------------------- + +This product contains a modified portion of 'Catapult', an open source +Trace Event viewer for Chome, Linux, and Android applications, which can +be obtained at: + + * LICENSE: + * traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/LICENSE (New BSD License) + * HOMEPAGE: + * https://github.com/catapult-project/catapult + +This product contains a modified portion of 'Polymer', a library for Web +Components, which can be obtained at: + * LICENSE: + * traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/polymer/LICENSE (New BSD License) + * HOMEPAGE: + * https://github.com/Polymer/polymer + + +This product contains a modified portion of 'ASM', an open source +Java Bytecode library, which can be obtained at: + + * LICENSE: + * agent/src/main/resources/io/perfmark/agent/third_party/asm/LICENSE (BSD style License) + * HOMEPAGE: + * https://asm.ow2.io/
\ No newline at end of file @@ -0,0 +1,2 @@ +kirit@google.com +olegsh@google.com diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4ac57d --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# PerfMark + +![PerfMark Hummingbird](doc/perfmark.png "PerfMark") + +PerfMark is a low-overhead, manually-instrumented, tracing library for Java. Users can add the +tracing function calls to their code to see how long each part takes. + +## Features + +* **Very Low Overhead**: When enabled, tracing a function call adds about **70ns**. Tracing is + done in a lock-free, wait-free, thread local buffer, which avoids interfering with your + latency-sensitive code. + +* **Dynamically Enabled**: PerfMark can be enabled or disabled at runtime. When disabled, + PerfMark has *zero overhead*, taking advantage of the JIT compiler to remove the tracing. + +* **Inter-thread Communication**: Existing profilers have difficulty expressing which thread + wakes up and executes work on another thread. PerfMark allows users to express this + relationship explicitly, making for a clear picture of how code flows. + +* **Small Library Size**: The PerfMark tracing API is only *5 KB* in size, and has minimal + dependencies making it easy to include in other projects. If no backend for recording the trace + is present, the library safely disables itself. + +* **Multiple Java Versions**: The PerfMark API supports Java 6, making it easy to include on + older or constrained environments. Additionally, PerfMark includes optimized backends for + Java 6, Java 7, and Java 9. Each of these backends is automatically loaded at runtime + (if possible) and uses advanced JVM features for maximum speed. + +* **Chrome Trace Viewer Integration**: PerfMark can export to the Chrome Trace Event Format, + making it easy to view in your Web Browser. + +## Usage + +To use PerfMark, add the following dependencies to your `build.gradle`: +``` +dependencies { + implementation 'io.perfmark:perfmark-api:0.25.0' + // Only needed for applications, not libraries. + implementation 'io.perfmark:perfmark-traceviewer:0.25.0' +} +``` + +Or in your `pom.xml`: + +``` + <dependency> + <groupId>io.perfmark</groupId> + <artifactId>perfmark-api</artifactId> + <version>0.25.0</version> + </dependency> +``` + +In your code, add the PerfMark tracing calls like so: + +```java +Map<String, Header> parseHeaders(List<String> rawHeaders) { + try (TaskCloseable task = PerfMark.traceTask("Parse HTTP headers")) { + Map<String, String> headers = new HashMap<>(); + for (String rawHeader : rawHeaders) { + Header header = parseHeader(rawHeader); + headers.put(header.name(), header); + } + return headers; + } +} + +``` + +PerfMark can also be used to record asynchronous work: + +```java +Future<Response> buildResponse() { + try (TaskCloseable task = PerfMark.traceTask("Build Response")) { + Link link = PerfMark.linkOut(); + return executor.submit(() -> { + try (TaskCloseable task2 = PerfMark.traceTask("Async Response")) { + PerfMark.linkIn(link); + return new Response(/* ... */); + } + }); + } +} +``` + +To view the traces in your browser, generate the HTML: + +```java + PerfMark.setEnabled(true); + PerfMark.event("My Task"); + TraceEventViewer.writeTraceHtml(); +``` + +The output looks like: + +![PerfMark Hummingbird](doc/screenshot.png "PerfMark") + +## Configuration +PerfMark provides some System Properties that allow controlling how it initializes. These can be set +by providing them as JVM arguments. (e.g. `-Dio.perfmark.PerfMark.startEnabled=true`) + +* `io.perfmark.PerfMark.startEnabled` controls if PerfMark starts enabled. This boolean property + makes it possible to start tracing calls immediately. This is helpful when it's difficult + to invoke `setEnabled()` on PerfMark before task tracing calls have started. + +* `io.perfmark.PerfMark.debug` controls if PerfMark can log initializing steps. This property + exists to disable class loading of the logger package (currently `java.util.logging`). If + the debug property is set, the logger settings still need to be configured to report the logs. + By default, all PerfMark logs use level `FINE` (SLF4J `DEBUG`) or lower, which means that they + usually need additional setup to print. + + In addition to initialization, the debug property controls if other tracing failures can be + logged. When calls involving deferred execution are used (e.g. + `startTask(T, StringFunction<T>)`), the String function provided may throw an exception. In + these cases, the exception is silently ignored. This makes it easy to ensure the start/stop + call parity is maintained. To view these failures, the debug property can be set to log such + problems. As above, the PerfMark logger should be configured as well to report these. + +## Versioning and API Stability + +PerfMark uses Semantic Versioning, and thus will not break existing APIs within a minor version +update. PerfMark may need to disable some functionality, and thus may need to make some tracing +calls become No-ops. In such cases, it will remain safe to call these functions being recorded. + +## Users + +PerfMark was designed originally for [gRPC](https://github.com/grpc/grpc-java). It is also used +by [Zuul](https://github.com/Netflix/zuul). diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..6ae91aa --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,28 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +RULES_JVM_EXTERNAL_TAG = "4.0" + +RULES_JVM_EXTERNAL_SHA = "31701ad93dbfe544d597dbe62c9a1fdd76d81d8a9150c2bf1ecf928ecdf97169" + +http_archive( + name = "rules_jvm_external", + sha256 = RULES_JVM_EXTERNAL_SHA, + strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG, + url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG, +) + +load("@rules_jvm_external//:defs.bzl", "maven_install") + +maven_install( + artifacts = [ + "junit:junit:4.13.2", + "com.google.errorprone:error_prone_annotations:2.7.1", + "com.google.code.findbugs:jsr305:3.0.2", + "com.google.code.gson:gson:2.8.7", + "com.google.truth:truth:1.1.3", + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + ], +) diff --git a/agent/build.gradle.kts b/agent/build.gradle.kts new file mode 100644 index 0000000..eb572fd --- /dev/null +++ b/agent/build.gradle.kts @@ -0,0 +1,77 @@ +import groovy.util.Node + +buildscript { + extra.apply { + set("moduleName", "io.perfmark.agent") + } +} + +plugins { + id("com.github.johnrengelman.shadow") version "7.0.0" +} + +val jdkVersion = JavaVersion.VERSION_1_6 + +dependencies { + compileOnly(libs.jsr305) + compileOnly(libs.errorprone) + + implementation("org.ow2.asm:asm:9.1") + implementation("org.ow2.asm:asm-commons:9.1") + + testImplementation(project(":perfmark-api")) + testImplementation(project(":perfmark-impl")) + testImplementation(libs.truth) + testRuntimeOnly(project(":perfmark-java6")) +} + +tasks.named<JavaCompile>("compileJava") { + sourceCompatibility = jdkVersion.toString() + targetCompatibility = jdkVersion.toString() + + javaCompiler.set(javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(11)) + }) + + options.compilerArgs.add("-Xlint:-options") +} + +tasks.named<JavaCompile>("compileTestJava") { + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() +} + +tasks.named<Jar>("jar") { + // Make this not the default + archiveClassifier.value("original") + manifest { + attributes(mapOf( + "Premain-Class" to "io.perfmark.agent.PerfMarkAgent", + )) + } +} + +tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") { + // make sure this is THE jar, which removes the suffix. + archiveClassifier.value(null as String?) + + relocate("org.objectweb.asm", "io.perfmark.agent.shaded.org.objectweb.asm") +} + +publishing { + publications { + named<MavenPublication>("maven") { + pom.withXml { + val root = asNode() + + for (child in root.children()) { + val c = child as Node + if (c.name().toString().endsWith("dependencies")) { + root.remove(c) + break + } + } + } + } + } +}
\ No newline at end of file diff --git a/agent/src/main/java/io/perfmark/agent/MethodVisitorRecorder.java b/agent/src/main/java/io/perfmark/agent/MethodVisitorRecorder.java new file mode 100644 index 0000000..960d0f7 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/MethodVisitorRecorder.java @@ -0,0 +1,435 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.TypePath; + +/** + * This class records the "header" portion of the method visitor. + */ +class MethodVisitorRecorder extends MethodVisitor { + + private final int VISIT_PARAMETER = 1; + private final int VISIT_ANNOTATION_DEFAULT = 2; + + private final int ANNOTATION_VISIT = 3; + private final int ANNOTATION_VISIT_ENUM = 4; + private final int ANNOTATION_VISIT_ANNOTATION = 5; + private final int ANNOTATION_VISIT_ARRAY = 6; + private final int ANNOTATION_VISIT_END = 7; + + private final int VISIT_ANNOTATION = 8; + private final int VISIT_ANNOTABLE_PARAMETER_COUNT = 9; + private final int VISIT_PARAMETER_ANNOTATION = 10; + private final int VISIT_TYPE_ANNOTATION = 11; + private final int VISIT_ATTRIBUTE = 12; + + private final AnnotationVisitorRecorder annotationVisitorRecorder = new AnnotationVisitorRecorder(null); + + private int opsWidx; + private int intsWidx; + private int stringsWidx; + private int objectsWidx; + private int booleansWidx; + + private int[] ops = new int[0]; + private String[] strings = new String[0]; + private int[] ints = new int[0]; + private Object[] objects = new Object[0]; + private boolean[] booleans = new boolean[0]; + + int firstLine = -1; + int lastLine = -1; + + MethodVisitorRecorder(MethodVisitor delegate) { + // Have to pin to a specific version, since the method invocations may be different in a later release + super(Opcodes.ASM9, delegate); + } + + /* + * Docs for Method Visitor Say: + * + * A visitor to visit a Java method. The methods of this class must be called in the following order: + * ( visitParameter )* + * [ visitAnnotationDefault ] + * ( visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation visitTypeAnnotation | visitAttribute )* + * [ visitCode ( + * visitFrame | visit<i>X</i>Insn | visitLabel | visitInsnAnnotation | visitTryCatchBlock + * | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )* + * visitMaxs ] + * visitEnd. + * + * + * In addition, the visit<i>X</i>Insn and visitLabel methods must be called in the sequential order of the bytecode + * instructions of the visited code, visitInsnAnnotation must be called after the annotated instruction, + * visitTryCatchBlock must be called before the labels passed as arguments have been visited, + * visitTryCatchBlockAnnotation must be called after the corresponding try catch block has been visited, and the + * visitLocalVariable, visitLocalVariableAnnotation and visitLineNumber methods must be called after the labels passed + * as arguments have been visited. + */ + + @Override + public final void visitParameter(String name, int access) { + addOp(VISIT_PARAMETER); + addString(name); + addInt(access); + super.visitParameter(name, access); + } + + @Override + public final AnnotationVisitor visitAnnotationDefault() { + addOp(VISIT_ANNOTATION_DEFAULT); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitAnnotationDefault()); + } + + @Override + public final AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + addOp(VISIT_ANNOTATION); + addString(descriptor); + addBoolean(visible); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitAnnotation(descriptor, visible)); + } + + @Override + public final void visitAnnotableParameterCount(int parameterCount, boolean visible) { + addOp(VISIT_ANNOTABLE_PARAMETER_COUNT); + addInt(parameterCount); + addBoolean(visible); + super.visitAnnotableParameterCount(parameterCount, visible); + } + + @Override + public final AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { + addOp(VISIT_PARAMETER_ANNOTATION); + addInt(parameter); + addString(descriptor); + addBoolean(visible); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitParameterAnnotation(parameter, descriptor, visible)); + } + + @Override + public final AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String descriptor, boolean visible) { + addOp(VISIT_TYPE_ANNOTATION); + addInt(typeRef); + addObject(typePath); + addString(descriptor); + addBoolean(visible); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitTypeAnnotation(typeRef, typePath, descriptor, visible)); + } + + @Override + public final void visitAttribute(Attribute attribute) { + addOp(VISIT_ATTRIBUTE); + addObject(attribute); + super.visitAttribute(attribute); + } + + @Override + public final void visitLineNumber(int line, Label start) { + if (firstLine == -1) { + firstLine = line; + } + lastLine = line; + super.visitLineNumber(line, start); + } + + final void replay() { + if (mv != null) { + replay(mv); + } + } + + final void replay(MethodVisitor delegate) { + if (delegate == null) { + return; + } + int stringsRidx = 0; + int intsRidx = 0; + int objectsRidx = 0; + int booleansRidx = 0; + int annoWidx = 0; + AnnotationVisitor[] visitorStack = new AnnotationVisitor[0]; + for (int opsRidx = 0; opsRidx < opsWidx; opsRidx++) { + int op = ops[opsRidx]; + switch (op) { + case VISIT_PARAMETER: { + String name = getString(stringsRidx++); + int access = getInt(intsRidx++); + delegate.visitParameter(name, access); + break; + } + case VISIT_ANNOTATION_DEFAULT: { + AnnotationVisitor visitor = delegate.visitAnnotationDefault(); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, visitor); + break; + } + case ANNOTATION_VISIT: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + Object value = getObject(objectsRidx++); + if (currentVisitor != null) { + currentVisitor.visit(name, value); + } + break; + } + case ANNOTATION_VISIT_ENUM: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + String descriptor = getString(stringsRidx++); + String value = getString(stringsRidx++); + if (currentVisitor != null) { + currentVisitor.visitEnum(name, descriptor, value); + } + break; + } + case ANNOTATION_VISIT_ANNOTATION: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + String descriptor = getString(stringsRidx++); + AnnotationVisitor newVisitor = null; + if (currentVisitor != null) { + newVisitor = currentVisitor.visitAnnotation(name, descriptor); + } + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case ANNOTATION_VISIT_ARRAY: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + AnnotationVisitor newVisitor = null; + if (currentVisitor != null) { + newVisitor = currentVisitor.visitArray(name); + } + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case ANNOTATION_VISIT_END: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + visitorStack[--annoWidx] = null; + if (currentVisitor != null) { + currentVisitor.visitEnd(); + } + break; + } + case VISIT_ANNOTATION: { + String descriptor = getString(stringsRidx++); + boolean visible = getBoolean(booleansRidx++); + AnnotationVisitor newVisitor = delegate.visitAnnotation(descriptor, visible); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case VISIT_ANNOTABLE_PARAMETER_COUNT: { + int parameterCount = getInt(intsRidx++); + boolean visible = getBoolean(booleansRidx++); + delegate.visitAnnotableParameterCount(parameterCount, visible); + break; + } + case VISIT_PARAMETER_ANNOTATION: { + int parameter = getInt(intsRidx++); + String descriptor = getString(stringsRidx++); + boolean visible = getBoolean(booleansRidx++); + AnnotationVisitor newVisitor = delegate.visitParameterAnnotation(parameter, descriptor, visible); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case VISIT_TYPE_ANNOTATION: { + // (int typeRef, TypePath typePath, String descriptor, boolean visible) + int typeRef = getInt(intsRidx++); + TypePath typePath = (TypePath) getObject(objectsRidx++); + String descriptor = getString(stringsRidx++); + boolean visible = getBoolean(booleansRidx++); + AnnotationVisitor newVisitor = delegate.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case VISIT_ATTRIBUTE: { + Attribute attribute = (Attribute) getObject(objectsRidx++); + delegate.visitAttribute(attribute); + break; + } + default: + throw new AssertionError("Bad op " + op); + } + } + } + + private void addOp(int op) { + ops = addInt(ops, opsWidx++, op); + } + + private void addInt(int value) { + ints = addInt(ints, intsWidx++, value); + } + + private static int[] addInt(int[] dest, int pos, int value) { + if (dest.length == pos){ + int[] newDest = new int[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private void addString(String value) { + strings = addString(strings, stringsWidx++, value); + } + + private static String[] addString(String[] dest, int pos, String value) { + if (dest.length == pos){ + String[] newDest = new String[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private void addObject(Object value) { + objects = addObject(objects, objectsWidx++, value); + } + + private static Object[] addObject(Object[] dest, int pos, Object value) { + if (dest.length == pos){ + Object[] newDest = new Object[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private void addBoolean(boolean value) { + booleans = addBoolean(booleans, booleansWidx++, value); + } + + private static boolean[] addBoolean(boolean[] dest, int pos, boolean value) { + if (dest.length == pos){ + boolean[] newDest = new boolean[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private static AnnotationVisitor[] addAnnotationVisitor(AnnotationVisitor[] dest, int pos, AnnotationVisitor value) { + if (dest.length == pos){ + AnnotationVisitor[] newDest = new AnnotationVisitor[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private int getInt(int ridx) { + assert ridx < intsWidx; + return ints[ridx]; + } + + private String getString(int ridx) { + assert ridx < stringsWidx; + return strings[ridx]; + } + + private Object getObject(int ridx) { + assert ridx < objectsWidx; + return objects[ridx]; + } + + private boolean getBoolean(int ridx) { + assert ridx < booleansWidx; + return booleans[ridx]; + } + + private final class AnnotationVisitorRecorder extends AnnotationVisitor { + + AnnotationVisitorRecorder(AnnotationVisitor delegate) { + super(MethodVisitorRecorder.this.api, delegate); + } + + /* + * Docs for AnnotationVisitor say + * + * A visitor to visit a Java annotation. The methods of this class must be called in the following order: + * ( visit | visitEnum | visitAnnotation | visitArray )* visitEnd. + */ + + @Override + public void visit(String name, Object value) { + addOp(ANNOTATION_VISIT); + addString(name); + addObject(value); + super.visit(name, value); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + addOp(ANNOTATION_VISIT_ENUM); + addString(name); + addString(descriptor); + addString(value); + super.visitEnum(name, descriptor, value); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String descriptor) { + addOp(ANNOTATION_VISIT_ANNOTATION); + addString(name); + addString(descriptor); + if (av == null) { + return this; + } + return new AnnotationVisitorRecorder(super.visitAnnotation(name, descriptor)); + } + + @Override + public AnnotationVisitor visitArray(String name) { + addOp(ANNOTATION_VISIT_ARRAY); + addString(name); + if (av == null) { + return this; + } + return new AnnotationVisitorRecorder(super.visitArray(name)); + } + + @Override + public void visitEnd() { + addOp(ANNOTATION_VISIT_END); + super.visitEnd(); + } + } +} diff --git a/agent/src/main/java/io/perfmark/agent/MethodWrappingWriter.java b/agent/src/main/java/io/perfmark/agent/MethodWrappingWriter.java new file mode 100644 index 0000000..46dd6e2 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/MethodWrappingWriter.java @@ -0,0 +1,164 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +final class MethodWrappingWriter { + + private final MethodVisitorRecorder recorder; + private final int access; + private final String methodName; + private final String descriptor; + private final String signature; + private final String[] exceptions; + private final String className; + private final String bodyMethodName; + private final ClassVisitor classVisitor; + private final boolean isInterface; + + MethodWrappingWriter( + MethodVisitorRecorder recorder, int access, String methodName, String descriptor, String signature, + String[] exceptions, boolean isInterface, String className, String bodyMethodName, ClassVisitor cv) { + this.recorder = recorder; + this.access = access; + this.methodName = methodName; + this.descriptor = descriptor; + this.signature = signature; + this.exceptions = exceptions; + this.isInterface = isInterface; + this.className = className; + this.bodyMethodName = bodyMethodName; + this.classVisitor = cv; + } + + void visit() { + MethodVisitor mv = classVisitor.visitMethod(access, methodName, descriptor, signature, exceptions); + if (mv == null) { + return; + } + // TODO(carl-mastrangelo): add start / stop call tags here + recorder.replay(mv); + + mv.visitCode(); + Label start = new Label(); + Label end = new Label(); + mv.visitLabel(start); + mv.visitLineNumber(recorder.firstLine, start); + mv.visitLdcInsn(className + "::" + methodName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "io/perfmark/PerfMark", "startTask", "(Ljava/lang/String;)V", false); + + if (((access & Opcodes.ACC_STATIC) == 0)) { + mv.visitVarInsn(Opcodes.ALOAD, 0); + } + + int params = 1; + char ret = 0; + assert descriptor.charAt(0) == '('; + out: for (int i = 1; i < descriptor.length(); i++) { + char c = descriptor.charAt(i); + switch (c) { + case ')': + ret = descriptor.charAt(i + 1); + break out; + case 'L': + mv.visitVarInsn(Opcodes.ALOAD, params++); + i = descriptor.indexOf(';', i); + assert i > 0; + break; + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + mv.visitVarInsn(Opcodes.ILOAD, params++); + break; + case 'F': + mv.visitVarInsn(Opcodes.FLOAD, params++); + break; + case 'J': + mv.visitVarInsn(Opcodes.LLOAD, params++); + break; + case 'D': + mv.visitVarInsn(Opcodes.DLOAD, params++); + break; + case '[': + mv.visitVarInsn(Opcodes.ALOAD, params++); + while (descriptor.charAt(++i) == '[') { + // empty body on purpose. + } + if (descriptor.charAt(i) == 'L') { + i = descriptor.indexOf(';', i); + } + + break; + default: + throw new RuntimeException("Bad descriptor " + c + " in " + descriptor); + } + } + + int invoke; + if ((access & Opcodes.ACC_STATIC) != 0) { + invoke = Opcodes.INVOKESTATIC; + } else if (isInterface) { + invoke = Opcodes.INVOKEINTERFACE; + } else { + invoke = Opcodes.INVOKESPECIAL; + } + + mv.visitMethodInsn(invoke, className.replace(".", "/"), bodyMethodName, descriptor, isInterface); + + mv.visitLabel(end); + mv.visitLineNumber(recorder.lastLine, end); + mv.visitLdcInsn(className + ":::" + methodName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "io/perfmark/PerfMark", "stopTask", "(Ljava/lang/String;)V", false); + + switch (ret) { + case 'V': + mv.visitInsn(Opcodes.RETURN); + break; + case 'L': + case '[': + mv.visitInsn(Opcodes.ARETURN); + break; + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + mv.visitInsn(Opcodes.IRETURN); + break; + case 'F': + mv.visitInsn(Opcodes.FRETURN); + break; + case 'J': + mv.visitInsn(Opcodes.LRETURN); + break; + case 'D': + mv.visitInsn(Opcodes.DRETURN); + break; + default: + throw new RuntimeException("Bad Descriptor " + ret); + } + + mv.visitMaxs(params, params + 1); + mv.visitEnd(); + } +} diff --git a/agent/src/main/java/io/perfmark/agent/NonMergingClassWriter.java b/agent/src/main/java/io/perfmark/agent/NonMergingClassWriter.java new file mode 100644 index 0000000..c93c648 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/NonMergingClassWriter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +/** + * This class exists to avoid the runtime lookup of classes when doing flow analysis. When + * computing frames, the default implementation needs to merge types together, which involves + * finding their common super class. This makes the transformer accidentally load a bunch of + * classes before they can be transformed. To avoid the possibility of this happening this + * class writer intentionally fails any such attempts. This ensures flags like {@link + * ClassWriter#COMPUTE_FRAMES} don't trigger merge logic. + */ +final class NonMergingClassWriter extends ClassWriter { + NonMergingClassWriter(ClassReader classReader, int flags) { + super(classReader, flags); + } + + @Override + protected String getCommonSuperClass(String type1, String type2) { + throw new UnsupportedOperationException("avoiding reflective lookup of classes"); + } +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkAgent.java b/agent/src/main/java/io/perfmark/agent/PerfMarkAgent.java new file mode 100644 index 0000000..ab4b2e9 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkAgent.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.agent; + +import java.lang.instrument.Instrumentation; + +public final class PerfMarkAgent { + + /** + * Entry point as an agent. + */ + public static void premain(String agentArgs, Instrumentation inst) { + inst.addTransformer(new PerfMarkTransformer(inst)); + } + + private PerfMarkAgent() {} +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkClassVisitor.java b/agent/src/main/java/io/perfmark/agent/PerfMarkClassVisitor.java new file mode 100644 index 0000000..3833068 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkClassVisitor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.ModuleVisitor; +import org.objectweb.asm.Opcodes; + +final class PerfMarkClassVisitor extends ClassVisitor { + + static final String[] ALL_METHODS = new String[0]; + + private final Runnable changeDetector = new ChangeDetector(); + + private final String classLoaderName; + private final String className; + private final String[] methodsToRewrite; + private final String[] methodsToWrap; + + private String fileName; + private String moduleName; + private String moduleVersion; + private boolean isInterface; + @SuppressWarnings("unused") + private boolean madeChanges; + + PerfMarkClassVisitor( + String classLoaderName, String className, String[] methodsToRewrite, String[] methodsToWrap, + ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + this.classLoaderName = classLoaderName; + this.className = className; + this.methodsToRewrite = methodsToRewrite == ALL_METHODS ? ALL_METHODS : methodsToRewrite.clone(); + this.methodsToWrap = methodsToWrap == ALL_METHODS ? ALL_METHODS : methodsToWrap.clone(); + } + + @Override + public ModuleVisitor visitModule(String name, int access, String version) { + this.moduleName = name; + this.moduleVersion = version; + return super.visitModule(name, access, version); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitSource(String source, String debug) { + this.fileName = source; + super.visitSource(source, debug); + } + + @Override + public MethodVisitor visitMethod( + int access, String methodName, String descriptor, String signature, String[] exceptions) { + final MethodVisitor superDelegate; + final MethodWrappingWriter methodWrapper; + if (shouldWrap(methodName, access)) { + if (className.startsWith("io/perfmark/") || className.startsWith("jdk/")) { + // Avoid recursion for now. + return super.visitMethod(access, methodName, descriptor, signature, exceptions); + } + String bodyMethodName = methodName + "$perfmark"; + int newAccess; + if (!isInterface) { + newAccess = (access | Opcodes.ACC_PRIVATE) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC); + } else { + newAccess = access; + } + MethodVisitor visitor = super.visitMethod(newAccess, bodyMethodName, descriptor, signature, exceptions); + MethodVisitorRecorder recorder = new MethodVisitorRecorder(visitor); + superDelegate = recorder; + methodWrapper = + new MethodWrappingWriter( + recorder, access, methodName, descriptor, signature, exceptions, isInterface, className, bodyMethodName, + cv); + } else { + superDelegate = super.visitMethod(access, methodName, descriptor, signature, exceptions); + methodWrapper = null; + } + + final MethodVisitor visitor; + if (shouldRewrite(methodName)) { + visitor = new PerfMarkMethodRewriter( + classLoaderName, moduleName, moduleVersion, className, methodName, fileName, changeDetector, superDelegate); + } else { + visitor = superDelegate; + } + + return new MethodVisitorWrapper(methodWrapper, visitor); + } + + boolean shouldWrap(String methodName, int access) { + if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) { + return false; + } + if (methodsToWrap == ALL_METHODS) { + // not yet supported + if (methodName.equals("<init>") || methodName.equals("<clinit>")) { + return false; + } + return true; + } + for (String method : methodsToWrap) { + if (method.equals(methodName)) { + return true; + } + } + return false; + } + + boolean shouldRewrite(String methodName) { + if (methodsToRewrite == ALL_METHODS) { + return true; + } + for (String method : methodsToRewrite) { + if (method.equals(methodName)) { + return true; + } + } + return false; + } + + private final class MethodVisitorWrapper extends MethodVisitor { + + private final MethodWrappingWriter methodWrapper; + + /** + * + * @param methodWrapper the writer of the "wrapper" method. May be {@code null}. + * @param delegate method visitor to inject in the additional method. May be {@code null}. + */ + MethodVisitorWrapper(MethodWrappingWriter methodWrapper, MethodVisitor delegate) { + super(PerfMarkClassVisitor.this.api, delegate); + this.methodWrapper = methodWrapper; + } + + @Override + public void visitEnd() { + super.visitEnd(); + if (methodWrapper != null) { + madeChanges = true; + methodWrapper.visit(); + } + } + } + + private final class ChangeDetector implements Runnable { + @Override + public void run() { + madeChanges = true; + } + } +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkMethodRewriter.java b/agent/src/main/java/io/perfmark/agent/PerfMarkMethodRewriter.java new file mode 100644 index 0000000..8a876b6 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkMethodRewriter.java @@ -0,0 +1,175 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Modified PerfMark startTask and stopTask call sits to include tags about where in code they came from. + */ +final class PerfMarkMethodRewriter extends MethodVisitor { + + private static final String PERFMARK_CLZ = "io/perfmark/PerfMark"; + private static final String TASKCLOSEABLE_CLZ = "io/perfmark/TaskCloseable"; + + /** + * May be {@code null} if absent. + */ + private static final Constructor<? extends StackTraceElement> MODULE_CTOR = getStackTraceElementCtorSafe(); + + private final Runnable onChange; + private final String classLoaderName; + private final String moduleName; + private final String moduleVersion; + // The internal fully qualified name. (e.g. java/lang/String) + private final String className; + private final String methodName; + private final String fileName; + + private int lineNumber = -1; + + /** + * Builds the rewriter to add debug into to trace calls. + * + * @param classLoaderName the loader of this class. May be {@code null}. + * @param moduleName the module of this class. May be {@code null}. + * @param moduleVersion the module version of this class. May be {@code null}. + * @param className the class that contains this method. Must be non-{@code null}. + * @param methodName the method currently being visited. Must be non-{@code null}. + * @param fileName the class that contains this method. May be {@code null}. + * @param onChange runnable to invoke if any changes are made to the class definition. May be {@code null}. + * @param methodVisitor the delegate to call. May be {@code null}. + */ + PerfMarkMethodRewriter( + String classLoaderName, String moduleName, String moduleVersion, String className, String methodName, + String fileName, Runnable onChange, MethodVisitor methodVisitor) { + super(Opcodes.ASM9, methodVisitor); + this.classLoaderName = classLoaderName; + this.moduleName = moduleName; + this.moduleVersion = moduleVersion; + this.className = className; + this.methodName = methodName; + this.fileName = fileName; + this.onChange = onChange; + } + + @Override + public void visitLineNumber(int line, Label start) { + lineNumber = line; + super.visitLineNumber(line, start); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (shouldPreTag(opcode, owner, name)) { + visitLdcInsn("PerfMark.stopCallSite"); + visitLdcInsn(callSite()); + super.visitMethodInsn(INVOKESTATIC, PERFMARK_CLZ, "attachTag", "(Ljava/lang/String;Ljava/lang/String;)V", false); + if (onChange != null) { + onChange.run(); + } + } + + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + + if (shouldPostTag(opcode, owner, name)) { + visitLdcInsn("PerfMark.startCallSite"); + visitLdcInsn(callSite()); + super.visitMethodInsn(INVOKESTATIC, PERFMARK_CLZ, "attachTag", "(Ljava/lang/String;Ljava/lang/String;)V", false); + if (onChange != null) { + onChange.run(); + } + } + } + + private boolean shouldPreTag(int opcode, String owner, String methodName) { + switch (opcode) { + case INVOKESTATIC: + return PERFMARK_CLZ.equals(owner) && methodName.equals("stopTask") && !TASKCLOSEABLE_CLZ.equals(className); + case INVOKEVIRTUAL: + return TASKCLOSEABLE_CLZ.equals(owner) && methodName.equals("close"); + default: + return false; + } + } + + private boolean shouldPostTag(int opcode, String owner, String methodName) { + return opcode == INVOKESTATIC + && PERFMARK_CLZ.equals(owner) + && (methodName.equals("startTask") || methodName.equals("traceTask")); + } + + private String callSite() { + StackTraceElement elem = null; + try { + elem = moduleElement(); + } catch (Throwable t) { + // TODO(carl-mastrangelo): this should log. + } + if (elem == null) { + elem = new StackTraceElement(className, methodName, fileName, lineNumber); + } + return elem.toString(); + } + + /** + * Builds the current callsite. Visible for testing. + * + * @return stack trace element using the modern constructor, or {@code null} if absent. + */ + StackTraceElement moduleElement() throws InvocationTargetException, InstantiationException, IllegalAccessException { + StackTraceElement elem = null; + if (MODULE_CTOR != null) { + elem = MODULE_CTOR.newInstance( + classLoaderName, moduleName, moduleVersion, className, methodName, fileName, lineNumber); + } + return elem; + } + + private static Constructor<StackTraceElement> getStackTraceElementCtorSafe() { + try { + return getStackTraceElementCtor(); + } catch (Throwable t) { + return null; + } + } + + /** + * Gets the more modern constructor. Visible for testing. + * + * @return the module-based constructor, or {@code null} if it is absent. + */ + static Constructor<StackTraceElement> getStackTraceElementCtor() { + Constructor<StackTraceElement> ctor = null; + try { + ctor = StackTraceElement.class.getConstructor( + String.class, String.class, String.class, String.class, String.class, String.class, int.class); + } catch (NoSuchMethodException e) { + // normal on JDK 8 and below, but include an assert in case the descriptor was wrong. + assert StackTraceElement.class.getConstructors().length < 2 : e; + } + return ctor; + } +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkTransformer.java b/agent/src/main/java/io/perfmark/agent/PerfMarkTransformer.java new file mode 100644 index 0000000..e5ce204 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkTransformer.java @@ -0,0 +1,184 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.agent; + +import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.security.ProtectionDomain; +import java.util.jar.JarFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +final class PerfMarkTransformer implements ClassFileTransformer { + + /** May be {@code null}. */ + private static final Method CLASS_LOADER_GET_NAME = getClassLoaderNameMethodSafe(); + + @SuppressWarnings("unused") + private final Instrumentation instrumentation; + + /** + * @param instrumentation may be {@code null}. + */ + PerfMarkTransformer(Instrumentation instrumentation) { + this.instrumentation = instrumentation; + if (instrumentation != null) { + try { + URL url = getClass().getClassLoader().getResource("io/perfmark/PerfMark.class"); + int index; + if (url != null && "jar".equals(url.getProtocol()) && (index = url.getFile().indexOf("!/")) != -1) { + // The "file" is what appendToBootstrapClassLoaderSearch uses, so we will too. The jar format + // looks like "file:/home/jars/perfmark-api.jar:/io/perfmark/PerfMark.class", so we need to strip + // off the "file:" and the bangslash. + JarFile jf = new JarFile(url.getFile().substring(5, index)); + jf.close(); + instrumentation.appendToBootstrapClassLoaderSearch(jf); + } + } catch (IOException e ) { + throw new RuntimeException(e); + } + } + } + + + @Override + public byte[] transform( + ClassLoader loader, + final String className, + Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + System.err.println(" Attempting " + className); + try { + return transformInternal(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } catch (Exception e) { + System.err.println(e.toString()); + e.printStackTrace(System.err); + throw new RuntimeException(e); + } + } + + byte[] transformInternal( + ClassLoader loader, + final String className, + Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + assert !className.contains(".") : "Binary name with `.` detected rather than internal name"; + String classLoaderName = getClassLoaderName(loader); + return transform(classLoaderName, className, classfileBuffer); + } + + private static byte[] transform(String classLoaderName, String className, byte[] classfileBuffer) { + ClassReader cr = new ClassReader(classfileBuffer); + if (true) { + ClassWriter cw = new NonMergingClassWriter(cr, ClassWriter.COMPUTE_MAXS); + PerfMarkClassVisitor perfMarkClassVisitor = + new PerfMarkClassVisitor( + classLoaderName, className, PerfMarkClassVisitor.ALL_METHODS, new String[0], cw); + cr.accept(perfMarkClassVisitor, 0); + return cw.toByteArray(); + } + return null; + } + + static String deriveFileName(String className) { + String clzName = className.replace('/', '.'); + int dollar = clzName.indexOf('$'); + String fileName; + if (dollar == -1) { + fileName = clzName; + } else { + fileName = clzName.substring(0, dollar); + } + if (!fileName.isEmpty()) { + int dot = fileName.lastIndexOf('.'); + if (dot != -1) { + fileName = fileName.substring(dot + 1); + } + } + // TODO: this is broken for private top level classes. + if (!fileName.isEmpty()) { + fileName += ".java"; + } else { + fileName = null; + } + return fileName; + } + + /** + * Returns the name for the class loader. Visible for testing. + * + * @param loader the class loader. May be {@code null}. + * @return The name of the class loader, or {@code null} if unavailable + */ + static String getClassLoaderName(ClassLoader loader) { + if (loader == null) { + return null; + } + if (CLASS_LOADER_GET_NAME != null) { + try { + return (String) CLASS_LOADER_GET_NAME.invoke(loader); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Error) { + throw (Error) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + return null; + } + + private static Method getClassLoaderNameMethodSafe() { + try { + return getClassLoaderNameMethod(); + } catch (Throwable t) { + safeLog(t, "Can't get loader method"); + } + return null; + } + + /** + * Gets name method for the Class loader for JDK9+. Visible for testing. + * + * @return the {@code getName} method, or {@code null} if it is absent. + */ + static Method getClassLoaderNameMethod() { + try { + return ClassLoader.class.getMethod("getName"); + } catch (NoSuchMethodException e) { + safeLog(e, "getName method missing"); + // expected + } + return null; + } + + @SuppressWarnings("UnusedVariable") + private static void safeLog(Throwable t, String message, Object... args) { + // TODO(carl-mastrangelo): implement. + } +} diff --git a/agent/src/main/resources/io/perfmark/agent/third_party/asm/LICENSE b/agent/src/main/resources/io/perfmark/agent/third_party/asm/LICENSE new file mode 100644 index 0000000..c71bb7b --- /dev/null +++ b/agent/src/main/resources/io/perfmark/agent/third_party/asm/LICENSE @@ -0,0 +1,27 @@ +ASM: a very small and fast Java bytecode manipulation framework +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. diff --git a/agent/src/test/java/io/perfmark/agent/PerfMarkMethodRewriterTest.java b/agent/src/test/java/io/perfmark/agent/PerfMarkMethodRewriterTest.java new file mode 100644 index 0000000..3a3e1bc --- /dev/null +++ b/agent/src/test/java/io/perfmark/agent/PerfMarkMethodRewriterTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.google.common.truth.Truth; +import io.perfmark.PerfMark; +import io.perfmark.TaskCloseable; +import io.perfmark.impl.MarkList; +import io.perfmark.impl.Storage; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +@RunWith(JUnit4.class) +public class PerfMarkMethodRewriterTest { + + @Test + public void getStackTraceElementCtor_present() { + Constructor<StackTraceElement> ctor = PerfMarkMethodRewriter.getStackTraceElementCtor(); + assertNotNull(ctor); + } + + @Test + public void moduleElement() throws Exception { + PerfMarkMethodRewriter rewriter = + new PerfMarkMethodRewriter( + "loadername", "modulename", "moduleversion", "classname", "methodname", "filename", () -> {}, null); + rewriter.visitLineNumber(1234, new Label()); + StackTraceElement expected = + new StackTraceElement("loadername", "modulename", "moduleversion", "classname", "methodname", "filename", 1234); + + StackTraceElement ste = rewriter.moduleElement(); + + assertEquals(expected, ste); + } + + @Test + public void moduleElement_oldClass() throws Exception { + PerfMarkMethodRewriter rewriter = + new PerfMarkMethodRewriter( + null, null, null, "classname", "methodname", null, () -> {}, null); + rewriter.visitLineNumber(1234, new Label()); + StackTraceElement expected = + new StackTraceElement("classname", "methodname", null, 1234); + + StackTraceElement ste = rewriter.moduleElement(); + + assertEquals(expected, ste); + } + + @Test + public void rewriteClass_task() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + String expectedValue = + new StackTraceElement("loadername", "modulename", "moduleversion", "classname", "methodname", "filename", -1) + .toString(); + // remove the trailing paren ) to make string match easier. + expectedValue = expectedValue.substring(0, expectedValue.length() - 1); + ClassReader reader = new ClassReader(ClzToRewrite.class.getName()); + ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); + reader.accept(new ClassVisitor(Opcodes.ASM9, writer) { + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor delegate = super.visitMethod(access, name, descriptor, signature, exceptions); + return new PerfMarkMethodRewriter( + "loadername", "modulename", "moduleversion", "classname", "methodname", "filename", () -> {}, delegate); + } + }, 0); + Class<?> clz = MethodHandles.lookup().defineHiddenClass(writer.toByteArray(), false).lookupClass(); + + clz.getMethod("task").invoke(null); + + MarkList out = Storage.readForTest(); + Truth.assertThat(out).hasSize(4); + String start = out.get(1).getTagStringValue(); + Truth.assertThat(start).startsWith(expectedValue); + String end = out.get(2).getTagStringValue(); + // Hack to check the line number changed. + Truth.assertThat(start).isNotEqualTo(end); + } + + @Test + public void rewriteClass_closeable() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + String expectedValue = + new StackTraceElement("loadername", "modulename", "moduleversion", "classname", "methodname", "filename", -1) + .toString(); + // remove the trailing paren ) to make string match easier. + expectedValue = expectedValue.substring(0, expectedValue.length() - 1); + ClassReader reader = new ClassReader(ClzToRewrite.class.getName()); + ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); + reader.accept(new ClassVisitor(Opcodes.ASM9, writer) { + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor delegate = super.visitMethod(access, name, descriptor, signature, exceptions); + return new PerfMarkMethodRewriter( + "loadername", "modulename", "moduleversion", "classname", "methodname", "filename", () -> {}, delegate); + } + }, 0); + Class<?> clz = MethodHandles.lookup().defineHiddenClass(writer.toByteArray(), false).lookupClass(); + + clz.getMethod("closeable").invoke(null); + + MarkList out = Storage.readForTest(); + Truth.assertThat(out).hasSize(4); + String start = out.get(1).getTagStringValue(); + Truth.assertThat(start).startsWith(expectedValue); + String end = out.get(2).getTagStringValue(); + // Hack to check the line number changed. + Truth.assertThat(start).isNotEqualTo(end); + } + + @SuppressWarnings("UnusedMethod") + private static final class ClzToRewrite { + public static void task() { + // These two calls MUST happen on separate lines to ensure line reading works. + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + } + + public static void closeable() { + try (TaskCloseable closeable = PerfMark.traceTask("task")) { + // extra line to ensure line numbers are different. + } + } + } +} diff --git a/agent/src/test/java/io/perfmark/agent/PerfMarkTransformerTest.java b/agent/src/test/java/io/perfmark/agent/PerfMarkTransformerTest.java new file mode 100644 index 0000000..df5ea58 --- /dev/null +++ b/agent/src/test/java/io/perfmark/agent/PerfMarkTransformerTest.java @@ -0,0 +1,509 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.agent; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; + +import com.google.common.truth.Truth; +import io.perfmark.PerfMark; +import io.perfmark.TaskCloseable; +import io.perfmark.impl.Mark; +import io.perfmark.impl.Storage; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class PerfMarkTransformerTest { + + @Test + public void deriveFileName() { + String file = PerfMarkTransformer.deriveFileName("io/perfmark/Clz"); + + assertEquals("Clz.java", file); + } + + @Test + public void deriveFileName_innerClass() { + String file = PerfMarkTransformer.deriveFileName("io/perfmark/Clz$Inner"); + + assertEquals("Clz.java", file); + } + + @Test + @Ignore + public void transform_autoAnnotate() throws Exception { + // This test currently depends on the transformer treating this test class specially. + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzAutoRecord.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(2); + } + + @Test + public void transform_record() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.SomeRecord.class); + Constructor<?> ctor = clz.getConstructor(int.class); + ctor.setAccessible(true); + ctor.newInstance(2); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(4); + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$SomeRecord"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("<init>"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("TransformerTestClasses$SomeRecord"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("<init>"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + } + + @Test + public void transform_lambda() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzCtorLambda.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(4); + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$ClzCtorLambda"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("lambda"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("TransformerTestClasses$ClzCtorLambda"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("lambda"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + } + + @Test + public void transform_methodRef() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzWithMethodRefs.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(2); + // I'm not sure what to do with methodrefs, so just leave it alone for now. + } + + @Test + public void transform_interface() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = + transformAndLoad(TransformerTestClasses.Bar.class, TransformerTestClasses.InterfaceWithDefaults.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(10); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$InterfaceWithDefaults"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("record"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("TransformerTestClasses$InterfaceWithDefaults"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("record"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + + assertEquals(marks.get(4).withTaskName("task"), marks.get(4)); + + // Ignore the regular tag at 5 + + assertEquals(marks.get(6).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("TransformerTestClasses$InterfaceWithDefaults"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("record"); + + assertEquals(marks.get(7).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("TransformerTestClasses$InterfaceWithDefaults"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("record"); + + // Ignore the regular tag at 8 + + assertEquals(marks.get(9).withTaskName("task"), marks.get(9)); + } + + @Test + public void transform_link() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzWithLinks.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(6); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$ClzWithLinks"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("init"); + + // assume links have not been modified + + assertEquals(marks.get(4).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(4).getTagStringValue()).contains("TransformerTestClasses$ClzWithLinks"); + Truth.assertThat(marks.get(4).getTagStringValue()).contains("init"); + + assertEquals(marks.get(5).withTaskName("task"), marks.get(5)); + } + + @Test + public void transform_closeable() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzWithCloseable.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(4); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$ClzWithCloseable"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("init"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("TransformerTestClasses$ClzWithCloseable"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("init"); + + assertEquals(Mark.Operation.TASK_END_N1S0, marks.get(3).getOperation()); + } + + @Test + public void transform_wrongCloseable() throws Exception { + // If the wrong static type is used, the agent won't be able to instrument it. Add a test to document this + // behavior. + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzWithWrongCloseable.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(3); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$ClzWithWrongCloseable"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("init"); + + assertEquals(Mark.Operation.TASK_END_N1S0, marks.get(2).getOperation()); + } + + @Test + public void transform_wrongCloseable_autoCloseable() throws Exception { + // If the wrong static type is used, the agent won't be able to instrument it. Add a test to document this + // behavior. + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<? extends Closeable> clz = transformAndLoad(TaskCloseable.class).asSubclass(Closeable.class); + Constructor<? extends Closeable> ctor = clz.getDeclaredConstructor(); + ctor.setAccessible(true); + Closeable closeable = ctor.newInstance(); + closeable.close(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(1); + + assertEquals(Mark.Operation.TASK_END_N1S0, marks.get(0).getOperation()); + } + + @Test + public void transform_ctor() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzWithCtor.class); + Constructor<?> ctor = clz.getConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(10); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$ClzWithCtor"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("init"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("TransformerTestClasses$ClzWithCtor"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("init"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + + assertEquals(marks.get(4).withTaskName("task"), marks.get(4)); + + // Ignore the regular tag at 5 + + assertEquals(marks.get(6).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("TransformerTestClasses$ClzWithCtor"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("init"); + + assertEquals(marks.get(7).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("TransformerTestClasses$ClzWithCtor"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("init"); + + // Ignore the regular tag at 8 + + assertEquals(marks.get(9).withTaskName("task"), marks.get(9)); + } + + @Test + public void transform_init() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzWithInit.class); + Constructor<?> ctor = clz.getDeclaredConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(10); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$ClzWithInit"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("init"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("TransformerTestClasses$ClzWithInit"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("init"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + + assertEquals(marks.get(4).withTaskName("task"), marks.get(4)); + + // Ignore the regular tag at 5 + + assertEquals(marks.get(6).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("TransformerTestClasses$ClzWithInit"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("init"); + + assertEquals(marks.get(7).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("TransformerTestClasses$ClzWithInit"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("init"); + + // Ignore the regular tag at 8 + + assertEquals(marks.get(9).withTaskName("task"), marks.get(9)); + } + + @Test + public void transform_clinit() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(TransformerTestClasses.ClzWithClinit.class); + Constructor<?> ctor = clz.getDeclaredConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(10); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("TransformerTestClasses$ClzWithClinit"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("clinit"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("TransformerTestClasses$ClzWithClinit"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("clinit"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + + assertEquals(marks.get(4).withTaskName("task"), marks.get(4)); + + // Ignore the regular tag at 5 + + assertEquals(marks.get(6).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("TransformerTestClasses$ClzWithClinit"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("clinit"); + + assertEquals(marks.get(7).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("TransformerTestClasses$ClzWithClinit"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("clinit"); + + // Ignore the regular tag at 8 + + assertEquals(marks.get(9).withTaskName("task"), marks.get(9)); + } + + @Test + public void transform_toplevel() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(ClzFooter.class); + Constructor<?> ctor = clz.getDeclaredConstructor(); + ctor.setAccessible(true); + ctor.newInstance(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(10); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("ClzFooter"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("init"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("ClzFooter"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("init"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + + assertEquals(marks.get(4).withTaskName("task"), marks.get(4)); + + // Ignore the regular tag at 5 + + assertEquals(marks.get(6).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("ClzFooter"); + Truth.assertThat(marks.get(6).getTagStringValue()).contains("init"); + + assertEquals(marks.get(7).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("ClzFooter"); + Truth.assertThat(marks.get(7).getTagStringValue()).contains("init"); + + // Ignore the regular tag at 8 + + assertEquals(marks.get(9).withTaskName("task"), marks.get(9)); + } + + @Test + public void transform_anonymousClass() throws Exception { + PerfMark.setEnabled(true); + Storage.clearLocalStorage(); + + Class<?> clz = transformAndLoad(new Runnable() { + // avoid IntelliJ thinking this should be a lambda. + public volatile int a; + @Override + public void run() { + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + } + }.getClass()); + Constructor<?> ctor = clz.getDeclaredConstructor(PerfMarkTransformerTest.class); + ctor.setAccessible(true); + Runnable instance = (Runnable) ctor.newInstance(this); + instance.run(); + List<Mark> marks = Storage.readForTest(); + assertThat(marks).hasSize(4); + + assertEquals(marks.get(0).withTaskName("task"), marks.get(0)); + + assertEquals(marks.get(1).getTagKey(), "PerfMark.startCallSite"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("PerfMarkTransformerTest$"); + Truth.assertThat(marks.get(1).getTagStringValue()).contains("run"); + + assertEquals(marks.get(2).getTagKey(), "PerfMark.stopCallSite"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("PerfMarkTransformerTest$"); + Truth.assertThat(marks.get(2).getTagStringValue()).contains("run"); + + assertEquals(marks.get(3).withTaskName("task"), marks.get(3)); + } + + private static Class<?> transformAndLoad(Class<?> toLoad, Class<?> ...extra) throws IOException { + Map<String, Class<?>> toTransform = new HashMap<>(); + for (Class<?> clz : extra) { + toTransform.put(clz.getName(), clz); + } + toTransform.put(toLoad.getName(), toLoad); + try { + return new ClassLoader(toLoad.getClassLoader()) { + + @Override + protected Class<?> loadClass(String binaryClassName, boolean resolve) throws ClassNotFoundException { + Class<?> existing = toTransform.get(binaryClassName); + if (existing == null) { + return super.loadClass(binaryClassName, resolve); + } + String internalFullyQualifiedClassName = binaryClassName.replace('.', '/'); + String resourceName = internalFullyQualifiedClassName + ".class"; + byte[] data; + try (InputStream stream = getResourceAsStream(resourceName)) { + data = stream.readAllBytes(); + } catch (IOException e) { + throw new RuntimeException(e); + } + byte[] newClassBytes = + new PerfMarkTransformer(null).transformInternal( + this, internalFullyQualifiedClassName, existing, null, data); + if (newClassBytes == null) { + newClassBytes = data; + } + Class<?> newClass = defineClass(binaryClassName, newClassBytes, 0, newClassBytes.length); + if (resolve) { + resolveClass(newClass); + } + return newClass; + } + }.loadClass(toLoad.getName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/agent/src/test/java/io/perfmark/agent/TransformerTestClasses.java b/agent/src/test/java/io/perfmark/agent/TransformerTestClasses.java new file mode 100644 index 0000000..fcfa5ce --- /dev/null +++ b/agent/src/test/java/io/perfmark/agent/TransformerTestClasses.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.agent; + +import io.perfmark.Link; +import io.perfmark.PerfMark; +import io.perfmark.Tag; +import io.perfmark.TaskCloseable; +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +final class TransformerTestClasses { + + static final class ClzAutoRecord { + public ClzAutoRecord() { + recordMe(); + } + + void recordMe() { + // seemingly nothing here. + } + } + + record SomeRecord(@SuppressWarnings("unused") int hi) { + public SomeRecord { + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + } + } + + static final class ClzCtorLambda implements Executor { + public ClzCtorLambda() { + execute( + () -> { + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + }); + } + + @Override + public void execute(@Nullable final Runnable command) { + command.run(); + } + } + + static final class ClzWithClinit { + static { + Tag tag = PerfMark.createTag("tag", 1); + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + PerfMark.startTask("task", tag); + PerfMark.stopTask("task", tag); + } + } + + static final class ClzWithInit { + { + Tag tag = PerfMark.createTag("tag", 1); + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + PerfMark.startTask("task", tag); + PerfMark.stopTask("task", tag); + } + } + + static final class ClzWithCtor { + public ClzWithCtor() { + Tag tag = PerfMark.createTag("tag", 1); + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + PerfMark.startTask("task", tag); + PerfMark.stopTask("task", tag); + } + } + + static final class ClzWithLinks { + public ClzWithLinks() { + PerfMark.startTask("task"); + Link link = PerfMark.linkOut(); + PerfMark.linkIn(link); + PerfMark.stopTask("task"); + } + } + + static final class ClzWithCloseable { + public ClzWithCloseable() { + try (TaskCloseable discard = PerfMark.traceTask("task")) {} + } + } + + static final class ClzWithWrongCloseable { + public ClzWithWrongCloseable() throws IOException { + try (Closeable discard = PerfMark.traceTask("task")) {} + } + } + + public interface InterfaceWithDefaults { + default void record() { + Tag tag = PerfMark.createTag("tag", 1); + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + PerfMark.startTask("task", tag); + PerfMark.stopTask("task", tag); + } + } + + static final class Bar implements InterfaceWithDefaults { + public Bar() { + record(); + } + } + + static final class ClzWithMethodRefs { + public ClzWithMethodRefs() { + execute(PerfMark::startTask); + execute(PerfMark::stopTask); + } + + void execute(Consumer<String> method) { + method.accept("task"); + } + } + + private TransformerTestClasses() {} +} + +final class ClzFooter { + { + Tag tag = PerfMark.createTag("tag", 1); + PerfMark.startTask("task"); + PerfMark.stopTask("task"); + PerfMark.startTask("task", tag); + PerfMark.stopTask("task", tag); + } +} diff --git a/api/BUILD.bazel b/api/BUILD.bazel new file mode 100644 index 0000000..1b7418a --- /dev/null +++ b/api/BUILD.bazel @@ -0,0 +1,62 @@ +java_library( + name = "api", + visibility = ["//visibility:public"], + exports = [ + ":link", + ":perfmark", + ":tag", + ":stringfunction", + ], +) + +java_library( + name = "perfmark", + srcs = glob(["src/main/java/io/perfmark/PerfMark.java", + "src/main/java/io/perfmark/TaskCloseable.java"]), + deps = [ + ":impl", + ":link", + ":tag", + ":stringfunction", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "tag", + srcs = glob(["src/main/java/io/perfmark/Tag.java"]), + visibility = ["//:__subpackages__"], + deps = [ + "@maven//:com_google_code_findbugs_jsr305", + ], +) + +java_library( + name = "link", + srcs = glob(["src/main/java/io/perfmark/Link.java"]), + visibility = ["//:__subpackages__"], + deps = [ + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "stringfunction", + visibility = ["//:__subpackages__"], + srcs = glob(["src/main/java/io/perfmark/StringFunction.java"]), +) + +java_library( + name = "impl", + srcs = glob(["src/main/java/io/perfmark/Impl.java"]), + visibility = ["//:__subpackages__"], + deps = [ + ":link", + ":tag", + ":stringfunction", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..1b69f2e --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,85 @@ +plugins { + id "me.champeau.jmh" +} + +description = "PerfMark API" +ext.moduleName = "io.perfmark" +ext.jdkVersion = JavaVersion.VERSION_1_6 + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +compileJava { + sourceCompatibility = jdkVersion + targetCompatibility = jdkVersion + + options.compilerArgs.add("-Xlint:-options") +} + +dependencies { + compileOnly libs.jsr305, + libs.errorprone + testImplementation project(':perfmark-impl'), + libs.truth + testRuntimeOnly project(':perfmark-java6') + + jmh project(':perfmark-java9'), + project(':perfmark-java7') +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileJmhJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +java { + disableAutoTargetJvm() +} + +javadoc { + exclude 'io/perfmark/Impl**' +} + +jmh { + + timeOnIteration = "1s" + warmup = "1s" + fork = 400 + warmupIterations = 0 + + includes = ["ClassInit"] + profilers = ["cl"] + jvmArgs = ["-Dio.perfmark.debug=true"] + + /* + profilers = ["perfasm"] + + jvmArgs = [ + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+LogCompilation", + "-XX:LogFile=/tmp/blah.txt", + "-XX:+PrintAssembly", + "-XX:+PrintInterpreter", + "-XX:+PrintNMethods", + "-XX:+PrintNativeNMethods", + "-XX:+PrintSignatureHandlers", + "-XX:+PrintAdapterHandlers", + "-XX:+PrintStubCode", + "-XX:+PrintCompilation", + "-XX:+PrintInlining", + "-XX:+TraceClassLoading", + "-XX:PrintAssemblyOptions=syntax", + "-XX:PrintAssemblyOptions=intel" + ] + */ + + //duplicateClassesStrategy DuplicatesStrategy.INCLUDE +}
\ No newline at end of file diff --git a/api/src/jmh/java/io/perfmark/ClassInitBenchmark.java b/api/src/jmh/java/io/perfmark/ClassInitBenchmark.java new file mode 100644 index 0000000..0944b41 --- /dev/null +++ b/api/src/jmh/java/io/perfmark/ClassInitBenchmark.java @@ -0,0 +1,95 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +@Fork(1000) +@Warmup(iterations = 0) +@Measurement(iterations = 1) +public class ClassInitBenchmark { + + private ClassLoader loader; + + @Setup + public void setup() { + loader = new TestClassLoader(getClass().getClassLoader(), "io.perfmark.impl.SecretPerfMarkImpl$PerfMarkImpl"); + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public Object forName_noinit() throws Exception { + return Class.forName(PerfMark.class.getName(), false, loader); + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public Object forName_init() throws Exception { + return Class.forName(PerfMark.class.getName(), true, loader); + } + + private static class TestClassLoader extends ClassLoader { + + private final List<String> classesToDrop; + + TestClassLoader(ClassLoader parent, String ... classesToDrop) { + super(parent); + this.classesToDrop = Arrays.asList(classesToDrop); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (classesToDrop.contains(name)) { + // Throw an exception, just like the real one would. + throw new ClassNotFoundException(); + } + if (!name.startsWith("io.perfmark.")) { + return super.loadClass(name, resolve); + } + try (InputStream is = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) { + if (is == null) { + throw new ClassNotFoundException(name); + } + byte[] data = is.readAllBytes(); + Class<?> clz = defineClass(name, data, 0, data.length); + if (resolve) { + resolveClass(clz); + } + return clz; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/api/src/jmh/java/io/perfmark/EnabledBenchmark.java b/api/src/jmh/java/io/perfmark/EnabledBenchmark.java new file mode 100644 index 0000000..07ca90b --- /dev/null +++ b/api/src/jmh/java/io/perfmark/EnabledBenchmark.java @@ -0,0 +1,134 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +@State(Scope.Benchmark) +public class EnabledBenchmark { + private final Tag TAG = new Tag("tag", 2); + + @Param({"true", "false"}) + public boolean enabled; + + final String there = "there"; + + @Setup + public void setup() { + PerfMark.setEnabled(enabled); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void startStop() { + PerfMark.startTask("hi", TAG); + PerfMark.stopTask("hi", TAG); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void startStop_method() { + PerfMark.startTask("hi", String::valueOf); + PerfMark.stopTask(); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public Tag createTag() { + return PerfMark.createTag("tag", 2); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void link() { + Link link = PerfMark.linkOut(); + PerfMark.linkIn(link); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void event() { + PerfMark.event("hi", TAG); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void attachKeyedTag_ss() { + PerfMark.attachTag("hi", "there"); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void attachKeyedTag_sn() { + PerfMark.attachTag("hi", 934); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void attachKeyedTag_snn() { + PerfMark.attachTag("hi", 934, 5); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void attachKeyedTag_ss_methodRef() { + PerfMark.attachTag("hi", this, EnabledBenchmark::getStringValue); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void attachKeyedTag_ss_ctor() { + PerfMark.attachTag("hi", there, String::new); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void attachKeyedTag_ss_globalRef() { + PerfMark.attachTag("hi", this, ignore -> this.there); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void attachKeyedTag_ss_localRef() { + String bar = there; + PerfMark.attachTag("hi", this, ignore -> bar); + } + + static String getStringValue(EnabledBenchmark self) { + return self.there; + } +} diff --git a/api/src/main/java/io/perfmark/Impl.java b/api/src/main/java/io/perfmark/Impl.java new file mode 100644 index 0000000..ddd65ad --- /dev/null +++ b/api/src/main/java/io/perfmark/Impl.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark; + +import javax.annotation.Nullable; + +public class Impl { + static final String NO_TAG_NAME = ""; + static final long NO_TAG_ID = Long.MIN_VALUE; + /** + * This value is current {@link Long#MIN_VALUE}, but it could also be {@code 0}. The invariant + * {@code NO_LINK_ID == -NO_LINK_ID} must be maintained to work when PerfMark is disabled. + */ + private static final long NO_LINK_ID = Long.MIN_VALUE; + + static final Tag NO_TAG = new Tag(Impl.NO_TAG_NAME, Impl.NO_TAG_ID); + static final Link NO_LINK = new Link(Impl.NO_LINK_ID); + + /** The Noop implementation */ + protected Impl(Tag key) { + if (key != NO_TAG) { + throw new AssertionError("nope"); + } + } + + protected void setEnabled(boolean value) {} + + protected boolean setEnabled(boolean value, boolean overload) { + return false; + } + + protected <T> void startTask(T taskNameObject, StringFunction<? super T> taskNameFunc) {} + + protected void startTask(String taskName, Tag tag) {} + + protected void startTask(String taskName) {} + + protected void startTask(String taskName, String subTaskName) {} + + protected void event(String eventName, Tag tag) {} + + protected void event(String eventName) {} + + protected void event(String eventName, String subEventName) {} + + protected void stopTask() {} + + protected void stopTask(String taskName, Tag tag) {} + + protected void stopTask(String taskName) {} + + protected void stopTask(String taskName, String subTaskName) {} + + protected Link linkOut() { + return NO_LINK; + } + + protected void linkIn(Link link) {} + + protected void attachTag(Tag tag) {} + + protected void attachTag(String tagName, String tagValue) {} + + protected void attachTag(String tagName, long tagValue) {} + + protected void attachTag(String tagName, long tagValue0, long tagValue1) {} + + protected <T> void attachTag( + String tagName, T tagObject, StringFunction<? super T> stringFunction) {} + + protected Tag createTag(@Nullable String tagName, long tagId) { + return NO_TAG; + } + + @Nullable + protected static String unpackTagName(Tag tag) { + return tag.tagName; + } + + protected static long unpackTagId(Tag tag) { + return tag.tagId; + } + + protected static long unpackLinkId(Link link) { + return link.linkId; + } + + protected static Tag packTag(@Nullable String tagName, long tagId) { + return new Tag(tagName, tagId); + } + + protected static Link packLink(long linkId) { + return new Link(linkId); + } +} diff --git a/api/src/main/java/io/perfmark/Link.java b/api/src/main/java/io/perfmark/Link.java new file mode 100644 index 0000000..de6c9b0 --- /dev/null +++ b/api/src/main/java/io/perfmark/Link.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark; + +import com.google.errorprone.annotations.DoNotCall; + +/** + * A link represents a linkage between asynchronous tasks. A link is created inside of a started + * task. The resulting {@link Link} object can then be passed to other asynchronous tasks to + * associate them with the original task. + * + * <p>A source task may have multiple outbound links pointing to other tasks. However, calling + * {@code PerfMark.linkIn(Link)} only work if it is the first such call. Subsequent calls have no + * effect. + */ +public final class Link { + + final long linkId; + + Link(long linkId) { + this.linkId = linkId; + } + + /** DO NOT CALL, no longer implemented. Use {@link PerfMark#linkIn} instead. */ + @Deprecated + @DoNotCall + public void link() {} +} diff --git a/api/src/main/java/io/perfmark/PerfMark.java b/api/src/main/java/io/perfmark/PerfMark.java new file mode 100644 index 0000000..9ce8044 --- /dev/null +++ b/api/src/main/java/io/perfmark/PerfMark.java @@ -0,0 +1,597 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.DoNotCall; +import com.google.errorprone.annotations.MustBeClosed; +import java.lang.reflect.Method; + +/** + * PerfMark is a very low overhead tracing library. To use PerfMark, annotate the code that needs to + * be traced using the start and stop methods. For example: + * + * <pre>{@code + * PerfMark.startTask("parseMessage"); + * try { + * message = parse(bytes); + * } finally { + * PerfMark.stopTask("parseMessage"); + * } + * }</pre> + * + * <p>When PerfMark is enabled, these tracing calls will record the start and stop times of the + * given task. When disabled, PerfMark disables these tracing calls, resulting in no additional + * tracing overhead. PerfMark can be enabled or disabled using the {@link #setEnabled(boolean)}. By + * default, PerfMark starts off disabled. PerfMark can be automatically enabled by setting the + * System property {@code io.perfmark.PerfMark.startEnabled} to true. + * + * <p>Tasks represent the span of work done by some code, starting and stopping in the same thread. + * Each task is started using one of the {@code startTask} methods, and ended using one of + * {@code stopTask} methods. Each start must have a corresponding stop. While not required, + * it is good practice for the start and stop calls have matching arguments for clarity. Tasks + * form a "tree", with each child task starting after the parent has started, and stopping before + * the parent has stopped. The most recently started (and not yet stopped) task is used by the + * tagging and linking commands described below. + * + * <p>Tags are metadata about the task. Each {@code Tag} contains a String and/or a long that + * describes the task, such as an RPC name, or request ID. When PerfMark is disabled, the Tag + * objects are not created, avoiding overhead. Tags are useful for keeping track of metadata + * about a task(s) that doesn't change frequently, or needs to be applied at multiple layers. + * In addition to Tag objects, named-tags can be added to the current task using the + * {@code attachTag} methods. These allow including key-value like metadata with the task. + * + * <p>Links allow the code to represent relationships between different threads. When one thread + * initiates work for another thread (such as a callback), Links express the control flow. For + * example: + * + * <pre>{@code + * PerfMark.startTask("handleMessage"); + * try { + * Link link = PerfMark.linkOut(); + * message = parse(bytes); + * executor.execute(() -> { + * PerfMark.startTask("processMessage"); + * try { + * PerfMark.linkIn(link); + * handle(message); + * } finally { + * PerfMark.stopTask("processMessage"); + * } + * }); + * } finally { + * PerfMark.stopTask("handleMessage"); + * } + * }</pre> + * + * <p>Links are created inside the scope of the current task and are linked into the scope of + * another task. PerfMark will represent the causal relationship between these two tasks. Links + * have a many-many relationship, and can be reused. Like Tasks and Tags, when PerfMark is + * disabled, the Links returned are no-op implementations. + * + * <p>Events are a special kind of Task, which do not have a duration. In effect, they only have + * a single timestamp the represents a particular occurrence. Events are slightly more efficient + * than tasks while PerfMark is enabled, but cannot be used with Links or named-tags. + * + * @author Carl Mastrangelo + */ +public final class PerfMark { + /** + * Turns on or off PerfMark recording. Don't call this method too frequently; while neither on nor + * off have very high overhead, transitioning between the two may be slow. + * + * @param value {@code true} to enable PerfMark recording, or {@code false} to disable it. + * @return If the enabled value was changed. + */ + @CanIgnoreReturnValue + public static boolean setEnabled(boolean value) { + return impl.setEnabled(value, false); + } + + /** + * Marks the beginning of a task. If PerfMark is disabled, this method is a no-op. The name of the + * task should be a runtime-time constant, usually a string literal. Tasks with the same name can + * be grouped together for analysis later, so avoid using too many unique task names. + * + * <p>The tag is a run-time identifier for the task. It represents the dynamic part of the task, + * while the task name is the constant part of the task. While not always enforced, tags should + * not be {@code null}. + * + * @param taskName the name of the task. + * @param tag a user provided tag for the task. + */ + public static void startTask(String taskName, Tag tag) { + impl.startTask(taskName, tag); + } + + /** + * Marks the beginning of a task. If PerfMark is disabled, this method is a no-op. The name of the + * task should be a runtime-time constant, usually a string literal. Tasks with the same name can + * be grouped together for analysis later, so avoid using too many unique task names. + * + * @param taskName the name of the task. + */ + public static void startTask(String taskName) { + impl.startTask(taskName); + } + + /** + * Marks the beginning of a task. If PerfMark is disabled, this method is a no-op. The name of the + * task should be a runtime-time constant, usually a string literal. Tasks with the same name can + * be grouped together for analysis later, so avoid using too many unique task names. + * + * <p>This function has many more caveats than the {@link #startTask(String)} that accept a + * string. See the docs at {@link #attachTag(String, Object, StringFunction)} for a list of risks + * associated with passing a function. + * + * @param taskNameObject the name of the task. + * @param taskNameFunction the function that will convert the taskNameObject to a taskName + * @param <T> the object type to be stringified + * @since 0.22.0 + */ + public static <T> void startTask(T taskNameObject, StringFunction<? super T> taskNameFunction) { + impl.startTask(taskNameObject, taskNameFunction); + } + + /** + * Marks the beginning of a task. If PerfMark is disabled, this method is a no-op. The names of + * the task and subtask should be runtime-time constants, usually a string literal. Tasks with the + * same name can be grouped together for analysis later, so avoid using too many unique task + * names. + * + * @param taskName the name of the task. + * @param subTaskName the name of the sub task + * @since 0.20.0 + */ + public static void startTask(String taskName, String subTaskName) { + impl.startTask(taskName, subTaskName); + } + + /** + * Marks the beginning of a task. If PerfMark is disabled, this method is a no-op. The name of the + * task should be a runtime-time constant, usually a string literal. Tasks with the same name can + * be grouped together for analysis later, so avoid using too many unique task names. + * + * <p>The returned closeable is meant to be used in a try-with-resources block. Callers should not + * allow the returned closeable to be used outside of the try block that initiated the call. + * Unlike other closeables, it is not safe to call close() more than once. + * + * @param taskName the name of the task. + * @return a closeable that must be closed at the end of the task + * @since 0.23.0 + */ + @MustBeClosed + public static TaskCloseable traceTask(String taskName) { + impl.startTask(taskName); + return TaskCloseable.INSTANCE; + } + + /** + * Marks the beginning of a task. If PerfMark is disabled, this method is a no-op. The name of the + * task should be a runtime-time constant, usually a string literal. Tasks with the same name can + * be grouped together for analysis later, so avoid using too many unique task names. + * + * <p>This function has many more caveats than the {@link #traceTask(String)} that accept a + * string. See the docs at {@link #attachTag(String, Object, StringFunction)} for a list of risks + * associated with passing a function. Unlike other closeables, it is not safe to call close() + * more than once. + * + * @param taskNameObject the name of the task. + * @param taskNameFunction the function that will convert the taskNameObject to a taskName + * @param <T> the object type to be stringified + * @return a closeable that must be closed at the end of the task + * @since 0.23.0 + */ + @MustBeClosed + public static <T> TaskCloseable traceTask( + T taskNameObject, StringFunction<? super T> taskNameFunction) { + impl.startTask(taskNameObject, taskNameFunction); + return TaskCloseable.INSTANCE; + } + + /** + * Marks an event. Events are logically both a task start and a task end. Events have no duration + * associated. Events still represent the instant something occurs. If PerfMark is disabled, this + * method is a no-op. + * + * <p>The tag is a run-time identifier for the event. It represents the dynamic part of the event, + * while the event name is the constant part of the event. While not always enforced, tags should + * not be {@code null}. + * + * @param eventName the name of the event. + * @param tag a user provided tag for the event. + */ + public static void event(String eventName, Tag tag) { + impl.event(eventName, tag); + } + + /** + * Marks an event. Events are logically both a task start and a task end. Events have no duration + * associated. Events still represent the instant something occurs. If PerfMark is disabled, this + * method is a no-op. + * + * @param eventName the name of the event. + */ + public static void event(String eventName) { + impl.event(eventName); + } + + /** + * Marks an event. Events are logically both a task start and a task end. Events have no duration + * associated. Events still represent the instant something occurs. If PerfMark is disabled, this + * method is a no-op. + * + * @param eventName the name of the event. + * @param subEventName the name of the sub event. + * @since 0.20.0 + */ + public static void event(String eventName, String subEventName) { + impl.event(eventName, subEventName); + } + + /** + * Marks the end of a task. If PerfMark is disabled, this method is a no-op. + * + * <p>It is important that {@link #stopTask} always be called after starting a task, even in case + * of exceptions. Failing to do so may result in corrupted results. + * + * @since 0.22.0 + */ + public static void stopTask() { + impl.stopTask(); + } + + /** + * Marks the end of a task. If PerfMark is disabled, this method is a no-op. The task name and tag + * should match the ones provided to the corresponding {@link #startTask(String, Tag)}, if + * provided. If the task name or tag do not match, the implementation will prefer the starting + * name and tag. The name and tag help identify the task if PerfMark is enabled mid way through + * the task, or if the previous results have been overwritten. The name of the task should be a + * runtime-time constant, usually a string literal. Consider using {@link #stopTask()} instead. + * + * <p>It is important that {@link #stopTask} always be called after starting a task, even in case + * of exceptions. Failing to do so may result in corrupted results. + * + * @param taskName the name of the task being ended. + * @param tag the tag of the task being ended. + */ + public static void stopTask(String taskName, Tag tag) { + impl.stopTask(taskName, tag); + } + + /** + * Marks the end of a task. If PerfMark is disabled, this method is a no-op. The task name should + * match the ones provided to the corresponding {@link #startTask(String)}, if provided. If the + * task name does not match, the implementation will prefer the starting name. The name helps + * identify the task if PerfMark is enabled mid way through the task, or if the previous results + * have been overwritten. The name of the task should be a runtime-time constant, usually a string + * literal. Consider using {@link #stopTask()} instead. + * + * <p>It is important that {@link #stopTask} always be called after starting a task, even in case + * of exceptions. Failing to do so may result in corrupted results. + * + * @param taskName the name of the task being ended. + */ + public static void stopTask(String taskName) { + impl.stopTask(taskName); + } + + /** + * Marks the end of a task. If PerfMark is disabled, this method is a no-op. The task name should + * match the ones provided to the corresponding {@link #startTask(String, String)}, if provided. + * If the task name does not match, the implementation will prefer the starting name. The name + * helps identify the task if PerfMark is enabled mid way through the task, or if the previous + * results have been overwritten. The name of the task should be a runtime-time constant, usually + * a string literal. Consider using {@link #stopTask()} instead. + * + * <p>It is important that {@link #stopTask} always be called after starting a task, even in case + * of exceptions. Failing to do so may result in corrupted results. + * + * @param taskName the name of the task being ended. + * @param subTaskName the name of the sub task being ended. + * @since 0.20.0 + */ + public static void stopTask(String taskName, String subTaskName) { + impl.stopTask(taskName, subTaskName); + } + + /** + * Creates a tag with no name or numeric identifier. The returned instance is different based on + * if PerfMark is enabled or not. + * + * <p>This method is seldomly useful; users should generally prefer to use the overloads of + * methods that don't need a tag. An empty tag may be useful though when the tag of a group of + * tasks may change over time. + * + * @return a Tag that has no name or id. + */ + public static Tag createTag() { + return Impl.NO_TAG; + } + + /** + * Creates a tag with no name. The returned instance is different based on if PerfMark is enabled + * or not. The provided id does not have to be globally unique, but is instead meant to give + * context to a task. + * + * @param id a user provided identifier for this Tag. + * @return a Tag that has no name. + */ + public static Tag createTag(long id) { + return impl.createTag(Impl.NO_TAG_NAME, id); + } + + /** + * Creates a tag with no numeric identifier. The returned instance is different based on if + * PerfMark is enabled or not. The provided name does not have to be globally unique, but is + * instead meant to give context to a task. + * + * @param name a user provided name for this Tag. + * @return a Tag that has no numeric identifier. + */ + public static Tag createTag(String name) { + return impl.createTag(name, Impl.NO_TAG_ID); + } + + /** + * Creates a tag with both a name and a numeric identifier. The returned instance is different + * based on if PerfMark is enabled or not. Neither the provided name nor id has to be globally + * unique, but are instead meant to give context to a task. + * + * @param id a user provided identifier for this Tag. + * @param name a user provided name for this Tag. + * @return a Tag that has both a name and id. + */ + public static Tag createTag(String name, long id) { + return impl.createTag(name, id); + } + + /** + * DO NOT CALL, no longer implemented. Use {@link #linkOut} instead. + * + * @return a no-op link that + */ + @Deprecated + @DoNotCall + public static Link link() { + return Impl.NO_LINK; + } + + /** + * A link connects between two tasks that start asynchronously. When {@link #linkOut()} is called, + * an association between the most recently started task and a yet-to-be named task on another + * thread, is created. Links are a one-to-many relationship. A single started task can have + * multiple associated tasks on other threads. + * + * @since 0.17.0 + * @return A Link to be used in other tasks. + */ + public static Link linkOut() { + return impl.linkOut(); + } + + /** + * Associate this link with the most recently started task. There may be at most one inbound + * linkage per task: the first call to {@link #linkIn} decides which outbound task is the origin. + * + * @param link a link created inside of another task. + * @since 0.17.0 + */ + public static void linkIn(Link link) { + impl.linkIn(link); + } + + /** + * Attaches an additional tag to the current active task. The tag provided is independent of the + * tag used with {@link #startTask(String, Tag)} and {@link #stopTask(String, Tag)}. Unlike the + * two previous two task overloads, the tag provided to {@link #attachTag(Tag)} does not have to + * match any other tags in use. This method is useful for when you have the tag information after + * the task is started. + * + * <p>Here are some example usages: + * + * <p>Recording the amount of work done in a task: + * + * <pre> + * PerfMark.startTask("read"); + * byte[] data = file.read(); + * PerfMark.attachTag(PerfMark.createTag("bytes read", data.length)); + * PerfMark.stopTask("read"); + * </pre> + * + * <p>Recording a tag which may be absent on an exception: + * + * <pre> + * Socket s; + * Tag remoteTag = PerfMark.createTag(remoteAddress.toString()); + * PerfMark.startTask("connect", remoteTag); + * try { + * s = connect(remoteAddress); + * PerfMark.attachTag(PerfMark.createTag(s.getLocalAddress().toString()); + * } finally { + * PerfMark.stopTask("connect", remoteTag); + * } + * </pre> + * + * @since 0.18.0 + * @param tag the Tag to attach. + */ + public static void attachTag(Tag tag) { + impl.attachTag(tag); + } + + /** + * Attaches an additional keyed tag to the current active task. The tag provided is independent of + * the tag used with {@code startTask} and {@code stopTask}. This tag operation is different than + * {@link Tag} in that the tag value has an associated name (also called a key). The tag name and + * value are attached to the most recently started task, and don't have to match any other tags. + * This method is useful for when you have the tag information after the task is started. + * + * @param tagName The name of the value being attached + * @param tagValue The value to attach to the current task. + * @since 0.20.0 + */ + public static void attachTag(String tagName, String tagValue) { + impl.attachTag(tagName, tagValue); + } + + /** + * Attaches an additional keyed tag to the current active task. The tag provided is independent of + * the tag used with {@code startTask} and {@code stopTask}. This tag operation is different than + * {@link Tag} in that the tag value has an associated name (also called a key). The tag name and + * value are attached to the most recently started task, and don't have to match any other tags. + * This method is useful for when you have the tag information after the task is started. + * + * @param tagName The name of the value being attached + * @param tagValue The value to attach to the current task. + * @since 0.20.0 + */ + public static void attachTag(String tagName, long tagValue) { + impl.attachTag(tagName, tagValue); + } + + /** + * Attaches an additional keyed tag to the current active task. The tag provided is independent of + * the tag used with {@code startTask} and {@code stopTask}. This tag operation is different than + * {@link Tag} in that the tag values have an associated name (also called a key). The tag name + * and values are attached to the most recently started task, and don't have to match any other + * tags. This method is useful for when you have the tag information after the task is started. + * + * <p>This method may treat the given two longs as special. If the tag name contains the string + * "uuid" (case insensitive), the value may be treated as a single 128 bit value. An example + * usage: + * + * <pre> + * RPC rpc = ... + * PerfMark.startTask("sendRPC"); + * try { + * UUID u = rpc.uuid(); + * PerfMark.attachTag("rpc uuid", u.getMostSignificantBits(), u.getLeastSignificantBits()); + * send(rpc); + * } finally { + * PerfMark.stopTask("sendRPC"); + * } + * </pre> + * + * @param tagName The name of the value being attached + * @param tagValue0 The first value to attach to the current task. + * @param tagValue1 The second value to attach to the current task. + * @since 0.20.0 + */ + public static void attachTag(String tagName, long tagValue0, long tagValue1) { + impl.attachTag(tagName, tagValue0, tagValue1); + } + + /** + * Attaches an additional keyed tag to the current active task. The tag provided is independent of + * the tag used with {@code startTask} and {@code stopTask}. This tag operation is different than + * {@link Tag} in that the tag value has an associated name (also called a key). The tag name and + * value are attached to the most recently started task, and don't have to match any other tags. + * This method is useful for when you have the tag information after the task is started. + * + * <p>Unlike {@link #attachTag(String, String)}, this defers constructing the tagValue String + * until later, and avoids doing any work while PerfMark is disabled. Callers are expected to + * provide a method handle that can consume the {@code tagObject}, and produce a tagValue. For + * example: + * + * <pre>{@code + * Response resp = client.makeCall(request); + * PerfMark.attachTag("httpServerHeader", resp, r -> r.getHeaders().get("Server")); + * }</pre> + * + * <p>Also unlike {@link #attachTag(String, String)}, this function is easier to misuse. Prefer + * using the other attachTag methods unless you are confident you need this one. Be familiar with + * following issues: + * + * <ul> + * <li>Callers should be careful to not capture the {@code tagObject}, and instead use the + * argument to {@code stringFunction}. This avoids a memory allocation and possibly holding + * the tagObject alive longer than necessary. + * <li>The {@code stringFunction} should be idempotent, have no side effects, and be safe to + * invoke from other threads. If the string function references state that may be changed, + * callers must synchronize access. The string function may be called multiple times for the + * same tag object. Additionally, if {@code attachTag()} is called with the same tag object + * and string function multiple times, PerfMark may invoke the function only once. + * <li>The tag object may kept alive longer than normal, and prevent garbage collection from + * reclaiming it. If the tag object retains a large amount of resources, this may appear as + * a memory leak. The risk of this memory increase will need to be balanced with the cost of + * eagerly constructing the tag value string. Additionally, if the string function is a + * capturing lambda (refers to local or global state), the function itself may appear as a + * leak. + * <li>If the stringFunction is {@code null}, or if it throws an exception when called, the tag + * value will not be attached. It is implementation defined if such problems are reported + * (e.g. logged). Note that exceptions are expensive compared to PerfMark calls, and thus + * may slow down tracing. If an exception is thrown, or if the stringFunction is {@code + * null}, PerfMark may invoke other methods on the tag object or string function, such as + * {@code toString()} and {@code getClass()}. + * </ul> + * + * @param tagName The name of the value being attached + * @param tagObject The tag object which will passed to the stringFunction. + * @param stringFunction The function that will convert the object to + * @param <T> the type of tag object to be stringified + * @since 0.22.0 + */ + public static <T> void attachTag( + String tagName, T tagObject, StringFunction<? super T> stringFunction) { + impl.attachTag(tagName, tagObject, stringFunction); + } + + private static final Impl impl; + + static { + Impl instance = null; + Throwable err = null; + Class<?> clz = null; + try { + clz = Class.forName("io.perfmark.impl.SecretPerfMarkImpl$PerfMarkImpl"); + } catch (Throwable t) { + err = t; + } + if (clz != null) { + try { + instance = clz.asSubclass(Impl.class).getConstructor(Tag.class).newInstance(Impl.NO_TAG); + } catch (Throwable t) { + err = t; + } + } + if (instance != null) { + impl = instance; + } else { + impl = new Impl(Impl.NO_TAG); + } + if (err != null) { + try { + if (Boolean.getBoolean("io.perfmark.PerfMark.debug")) { + // We need to be careful here, as it's easy to accidentally cause a class load. Logger is loaded + // reflectively to avoid accidentally pulling it in. + // TODO(carl-mastrangelo): Maybe make this load SLF4J instead? + Class<?> logClass = Class.forName("java.util.logging.Logger"); + Object logger = logClass.getMethod("getLogger", String.class).invoke(null, PerfMark.class.getName()); + Class<?> levelClass = Class.forName("java.util.logging.Level"); + Object level = levelClass.getField("FINE").get(null); + Method logMethod = logClass.getMethod("log", levelClass, String.class, Throwable.class); + logMethod.invoke(logger, level, "Error during PerfMark.<clinit>", err); + } + } catch (Throwable e) { + // ignored. + } + } + } + + private PerfMark() {} +} diff --git a/api/src/main/java/io/perfmark/StringFunction.java b/api/src/main/java/io/perfmark/StringFunction.java new file mode 100644 index 0000000..19506e0 --- /dev/null +++ b/api/src/main/java/io/perfmark/StringFunction.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Google LLC + * + * 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 io.perfmark; + +/** + * This interface is equivalent to {@code java.util.function.Function}. It is here as a + * compatibility shim to make PerfMark compatible with Java 6. This will likely be removed if + * PerfMark picks Java 8 as the minimum support version. See {@link PerfMark} for expected usage. + * + * @since 0.22.0 + * @param <T> The type to turn into a String. + */ +public interface StringFunction<T> { + + /** + * Takes the given argument and produces a String. + * + * @since 0.22.0 + * @param t the subject to Stringify + * @return the String + */ + String apply(T t); +} diff --git a/api/src/main/java/io/perfmark/Tag.java b/api/src/main/java/io/perfmark/Tag.java new file mode 100644 index 0000000..f054a7e --- /dev/null +++ b/api/src/main/java/io/perfmark/Tag.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark; + +import javax.annotation.Nullable; + +/** Tag is a dynamic, runtime created identifier (such as an RPC id). */ +public final class Tag { + @Nullable final String tagName; + final long tagId; + + Tag(@Nullable String tagName, long tagId) { + // tagName should be non-null, but checking is expensive + this.tagName = tagName; + this.tagId = tagId; + } +} diff --git a/api/src/main/java/io/perfmark/TaskCloseable.java b/api/src/main/java/io/perfmark/TaskCloseable.java new file mode 100644 index 0000000..50c763b --- /dev/null +++ b/api/src/main/java/io/perfmark/TaskCloseable.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Google LLC + * + * 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 io.perfmark; + +import java.io.Closeable; + +/** + * TaskCloseable is a helper class to simplify the closing of PerfMark tasks. It should be used in a + * try-with-resources block so that PerfMark tasks are recorded even in the event of exceptions. + * + * <p>Implementation note: This would normally implement {@code AutoCloseable}, but that is not + * available in Java 6. A future version of PerfMark may implement the parent interface instead. + * + * + * @since 0.23.0 + */ +public final class TaskCloseable implements Closeable { + + static final TaskCloseable INSTANCE = new TaskCloseable(); + + /** + * Stops the opened task. See {@link PerfMark#traceTask(String)}. + */ + @Override + public void close() { + PerfMark.stopTask(); + } + + private TaskCloseable() {} +} diff --git a/api/src/main/java/io/perfmark/package-info.java b/api/src/main/java/io/perfmark/package-info.java new file mode 100644 index 0000000..1c4301e --- /dev/null +++ b/api/src/main/java/io/perfmark/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +@javax.annotation.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +package io.perfmark; diff --git a/api/src/test/java/io/perfmark/CompatibilityTest.java b/api/src/test/java/io/perfmark/CompatibilityTest.java new file mode 100644 index 0000000..51b383d --- /dev/null +++ b/api/src/test/java/io/perfmark/CompatibilityTest.java @@ -0,0 +1,452 @@ +/* + * Copyright 2022 Google LLC + * + * 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 io.perfmark; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class CompatibilityTest { + private static final int STABLE_VERSION = 15; + + private static final List<String> VERSIONS = + List.of( + "0.13.37", + "0.14.0", + "0.15.0", + "0.16.0", + "0.17.0", + // "0.18.0", missing, not sure why + "0.19.0", + "0.20.1", + "0.21.0", + "0.22.0", + "0.23.0", + "0.24.0", + "0.25.0"); + + @Parameterized.Parameters(name = "version v{0}") + @SuppressWarnings("StringSplitter") + public static Iterable<Object[]> params() { + List<Object[]> params = new ArrayList<>(); + for (var version : VERSIONS) { + String fileName = "perfmark-api-" + version + ".jar"; + URL jarPath = CompatibilityTest.class.getResource(fileName); + if (jarPath == null) { + throw new AssertionError("Can't load version " + version); + } + params.add(new Object[]{version, Integer.valueOf(version.split("\\.")[1]), jarPath}); + } + + return params; + } + + @Parameterized.Parameter(0) + public String semanticVersion; + + @Parameterized.Parameter(1) + public int minorVersion; + + @Parameterized.Parameter(2) + public URL jarPath; + + private final Class<?> currentPerfMarkClz = PerfMark.class; + + private Class<?> perfMarkClz; + private Class<?> storageClz; + + @Before + public void setUp() throws Exception { + ClassLoader loader = new ApiOverrideClassLoader(); + + perfMarkClz = Class.forName("io.perfmark.PerfMark", false, loader); + assertNotEquals(currentPerfMarkClz, perfMarkClz); + + storageClz = Class.forName("io.perfmark.impl.Storage", false, loader); + var marks = (List) storageClz.getMethod("readForTest").invoke(null); + assertThat(marks).isNull(); + } + + @After + public void tearDown() throws Exception { + if (storageClz != null) { + storageClz.getMethod("clearLocalStorage").invoke(null); + } + } + + @Test + public void checkPublicMethods_disabledOnMissingImpl() throws Exception { + Assume.assumeTrue(minorVersion >= STABLE_VERSION); + + ClassLoader loader = new ApiOverrideNoImplClassLoader(); + + Class<?> perfMarkClz = Class.forName("io.perfmark.PerfMark", false, loader); + assertNotEquals(currentPerfMarkClz, perfMarkClz); + assertNotEquals(this.perfMarkClz, perfMarkClz); + + var tag = perfMarkClz.getMethod("createTag").invoke(null); + var link = perfMarkClz.getMethod("link").invoke(null); + + for (Method method : perfMarkClz.getMethods()) { + if (!Modifier.isStatic(method.getModifiers())) { + continue; + } + var paramTypes = method.getParameterTypes(); + Object[] args = new Object[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i].getName().startsWith("io.perfmark.")) { + paramTypes[i] = Class.forName(paramTypes[i].getName(), false, currentPerfMarkClz.getClassLoader()); + } + if (paramTypes[i] == long.class) { + args[i] = 0L; + } else if (paramTypes[i] == boolean.class) { + args[i] = true; + } else if (paramTypes[i].getSimpleName().equals("Link")) { + args[i] = link; + } else if (paramTypes[i].getSimpleName().equals("Tag")) { + args[i] = tag; + } else if (Object.class.isAssignableFrom(paramTypes[i])) { + args[i] = null; + } else { + throw new AssertionError("unknown param"); + } + } + + method.invoke(null, args); + } + } + + @Test + public void checkPublicMethods() throws Exception { + Assume.assumeTrue(minorVersion >= STABLE_VERSION); + for (Method method : perfMarkClz.getMethods()) { + var paramTypes = method.getParameterTypes(); + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i].getName().startsWith("io.perfmark.")) { + paramTypes[i] = Class.forName(paramTypes[i].getName(), false, currentPerfMarkClz.getClassLoader()); + } + } + Class<?> returnType = method.getReturnType(); + if (returnType.getName().startsWith("io.perfmark.")) { + returnType = Class.forName(returnType.getName(), false, currentPerfMarkClz.getClassLoader()); + } + + var m = currentPerfMarkClz.getMethod(method.getName(), paramTypes); + assertNotNull(method.getName(), m); + if (returnType == void.class) { + return; + } + assertEquals(m.getReturnType(), returnType); + } + } + + @Test + public void startStopTaskWorks() throws Exception { + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("startTask", String.class).invoke(null, "task1"); + perfMarkClz.getMethod("stopTask", String.class).invoke(null, "task1"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + // 13 - 16 should safely disable themselves, nag, but ultimately produce no data. + if (minorVersion >= 17) { + assertThat(marks).hasSize(2); + } else { + assertThat(marks).isNull(); + } + } + + @Test + public void startStopTaskWorks_tag() throws Exception { + Class<?> tagClz = Class.forName("io.perfmark.Tag", false, perfMarkClz.getClassLoader()); + Object tag = perfMarkClz.getMethod("createTag").invoke(null); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("startTask", String.class, tagClz).invoke(null, "task1", tag); + perfMarkClz.getMethod("stopTask", String.class, tagClz).invoke(null, "task1", tag); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + // 13 - 16 should safely disable themselves, nag, but ultimately produce no data. + if (minorVersion >= 17) { + assertThat(marks).hasSize(4); + } else { + assertThat(marks).isNull(); + } + } + + @Test + public void startStopTaskWorks_namedFunction() throws Exception { + Assume.assumeTrue(minorVersion >= 22); + Class<?> fnClz = Class.forName("io.perfmark.StringFunction", false, perfMarkClz.getClassLoader()); + Object fn = + Proxy.newProxyInstance(perfMarkClz.getClassLoader(), new Class<?>[]{fnClz}, (proxy, method, args) -> "hi"); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("startTask", Object.class, fnClz).invoke(null, new Object(), fn); + perfMarkClz.getMethod("stopTask").invoke(null); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(2); + } + + @Test + public void startStopTaskWorks_subTask() throws Exception { + Assume.assumeTrue(minorVersion >= 20); + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("startTask", String.class, String.class).invoke(null, "task1", "sub"); + perfMarkClz.getMethod("stopTask", String.class, String.class).invoke(null, "task1", "sub"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(2); + } + + @Test + public void traceTask() throws Exception { + Assume.assumeTrue(minorVersion >= 23); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + try (var c = (Closeable) perfMarkClz.getMethod("traceTask", String.class).invoke(null, "task1")) {} + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(2); + } + + @Test + public void traceTask_namedFunction() throws Exception { + Assume.assumeTrue(minorVersion >= 23); + + Class<?> fnClz = Class.forName("io.perfmark.StringFunction", false, perfMarkClz.getClassLoader()); + Object fn = + Proxy.newProxyInstance(perfMarkClz.getClassLoader(), new Class<?>[]{fnClz}, (proxy, method, args) -> "hi"); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + try (var c = (Closeable) perfMarkClz.getMethod("traceTask", Object.class, fnClz).invoke(null, new Object(), fn)) {} + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(2); + } + + @Test + public void event_tag() throws Exception { + // Versions Prior to 15 Had a duration associated with an event that was removed before stability. This means + // Classloading in PerfMark init can't find the MarkHolder.event() incompatibility between older and later versions. + Assume.assumeTrue(minorVersion >= STABLE_VERSION); + + Class<?> tagClz = Class.forName("io.perfmark.Tag", false, perfMarkClz.getClassLoader()); + Object tag = perfMarkClz.getMethod("createTag").invoke(null); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("event", String.class, tagClz).invoke(null, "event1", tag); + perfMarkClz.getMethod("event", String.class).invoke(null, "event2"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + // 13 - 16 should safely disable themselves, nag, but ultimately produce no data. + if (minorVersion >= 17) { + assertThat(marks).hasSize(2); + } else { + assertThat(marks).isNull(); + } + } + + @Test + public void event_subEvent() throws Exception { + Assume.assumeTrue(minorVersion >= 20); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("event", String.class, String.class).invoke(null, "event1", "subevent"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(1); + } + + @Test + public void createTags() throws Exception { + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("createTag", long.class).invoke(null, 2); + perfMarkClz.getMethod("createTag", String.class).invoke(null, "tag2"); + perfMarkClz.getMethod("createTag", String.class, long.class).invoke(null, "tag2", 2); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).isNull(); + } + + @Test + public void link_doesNothing() throws Exception { + // This is broken in early versions, sorry. + Assume.assumeTrue(minorVersion >= STABLE_VERSION); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + perfMarkClz.getMethod("link").invoke(null); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).isNull(); + } + + @Test + public void linkInLinkOut() throws Exception { + Assume.assumeTrue(minorVersion >= 17); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + + perfMarkClz.getMethod("startTask", String.class).invoke(null, "task1"); + Object link = perfMarkClz.getMethod("linkOut").invoke(null); + perfMarkClz.getMethod("stopTask", String.class).invoke(null, "task1"); + + perfMarkClz.getMethod("startTask", String.class).invoke(null, "task2"); + perfMarkClz.getMethod("linkIn", link.getClass()).invoke(null, link); + perfMarkClz.getMethod("stopTask", String.class).invoke(null, "task2"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(6); + } + + @Test + public void attachTag_tag() throws Exception { + Assume.assumeTrue(minorVersion >= 18); + + Class<?> tagClz = Class.forName("io.perfmark.Tag", false, perfMarkClz.getClassLoader()); + Object tag = perfMarkClz.getMethod("createTag").invoke(null); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + + perfMarkClz.getMethod("startTask", String.class).invoke(null, "task1"); + perfMarkClz.getMethod("attachTag", tagClz).invoke(null, tag); + perfMarkClz.getMethod("stopTask", String.class).invoke(null, "task1"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(3); + } + + @Test + public void attachTag_namedTag() throws Exception { + Assume.assumeTrue(minorVersion >= 20); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + + perfMarkClz.getMethod("startTask", String.class).invoke(null, "task1"); + perfMarkClz.getMethod("attachTag", String.class, String.class).invoke(null, "name1", "val"); + perfMarkClz.getMethod("attachTag", String.class, long.class).invoke(null, "name2", 22L); + perfMarkClz.getMethod("attachTag", String.class, long.class, long.class).invoke(null, "uuid", 22L, 55L); + perfMarkClz.getMethod("stopTask", String.class).invoke(null, "task1"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(5); + } + + @Test + public void attachTag_namedFunction() throws Exception { + Assume.assumeTrue(minorVersion >= 22); + + Class<?> fnClz = Class.forName("io.perfmark.StringFunction", false, perfMarkClz.getClassLoader()); + Object fn = + Proxy.newProxyInstance(perfMarkClz.getClassLoader(), new Class<?>[]{fnClz}, (proxy, method, args) -> "hi"); + + perfMarkClz.getMethod("setEnabled", boolean.class).invoke(null, true); + + perfMarkClz.getMethod("startTask", String.class).invoke(null, "task1"); + perfMarkClz.getMethod("attachTag", String.class, Object.class, fnClz).invoke(null, "name1", new Object(), fn); + perfMarkClz.getMethod("stopTask", String.class).invoke(null, "task1"); + + List marks = (List) storageClz.getMethod("readForTest").invoke(null); + + assertThat(marks).hasSize(3); + } + + private final class ApiOverrideClassLoader extends ClassLoader { + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("io.perfmark.")) { + for (var loader : List.of(new URLClassLoader(new URL[] {jarPath}, null), getClass().getClassLoader())) { + try (var stream = loader.getResourceAsStream(name.replace('.', '/') + ".class")) { + if (stream == null) { + continue; + } + var data = stream.readAllBytes(); + var clz = defineClass(name, data, 0, data.length); + if (resolve) { + resolveClass(clz); + } + return clz; + } catch (IOException e) { + throw (ClassNotFoundException) new ClassNotFoundException().initCause(e); + } + } + throw new ClassNotFoundException("not in here: " + name); + } + return super.loadClass(name, resolve); + } + } + + private final class ApiOverrideNoImplClassLoader extends ClassLoader { + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("io.perfmark.")) { + if (name.startsWith("io.perfmark.impl.")) { + throw new ClassNotFoundException(name); + } + for (var loader : List.of(new URLClassLoader(new URL[] {jarPath}, null), getClass().getClassLoader())) { + try (var stream = loader.getResourceAsStream(name.replace('.', '/') + ".class")) { + if (stream == null) { + continue; + } + var data = stream.readAllBytes(); + var clz = defineClass(name, data, 0, data.length); + if (resolve) { + resolveClass(clz); + } + return clz; + } catch (IOException e) { + throw (ClassNotFoundException) new ClassNotFoundException().initCause(e); + } + } + throw new ClassNotFoundException("not in here: " + name); + } + return super.loadClass(name, resolve); + } + } +} diff --git a/api/src/test/java/io/perfmark/PerfMarkTest.java b/api/src/test/java/io/perfmark/PerfMarkTest.java new file mode 100644 index 0000000..5397b3f --- /dev/null +++ b/api/src/test/java/io/perfmark/PerfMarkTest.java @@ -0,0 +1,491 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark; + +import static io.perfmark.impl.Mark.NO_TAG_ID; +import static org.junit.Assert.assertEquals; + +import com.google.common.truth.Truth; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import io.perfmark.impl.Storage; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.security.Permission; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.PropertyPermission; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.logging.LoggingPermission; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class PerfMarkTest { + + /** + * This test checks to see if PerfMark can be used from a Logger, which is used for recording if there is trouble + * turning on. PerfMark should set a noop implementation before recording any problems with boot. + */ + @Test + public void noBootCycle() throws Exception { + AtomicReference<LogRecord> ref = new AtomicReference<>(); + ClassLoader loader = + new TestClassLoader( + getClass().getClassLoader(), "io.perfmark.impl.SecretPerfMarkImpl$PerfMarkImpl"); + Class<?> clz = Class.forName(PerfMark.class.getName(), false, loader); + + Class<?> filterClz = Class.forName(TracingFilter.class.getName(), false, loader); + Constructor<? extends Filter> ctor = filterClz.asSubclass(Filter.class) + .getDeclaredConstructor(Class.class, AtomicReference.class); + ctor.setAccessible(true); + Filter filter = ctor.newInstance(clz, ref); + Logger logger = Logger.getLogger(PerfMark.class.getName()); + Level oldLevel = logger.getLevel(); + Filter oldFilter = logger.getFilter(); + logger.setLevel(Level.ALL); + logger.setFilter(filter); + try { + runWithProperty(System.getProperties(), "io.perfmark.PerfMark.debug", "true", () -> { + try { + // Force Initialization. + Class.forName(PerfMark.class.getName(), true, loader); + } finally{ + logger.setFilter(oldFilter); + } + return null; + }); + } finally{ + logger.setFilter(oldFilter); + logger.setLevel(oldLevel); + } + + // The actual SecretPerfMarkImpl is not part of the custom class loader above, so it will be a class mismatch when + // it tries to implement Impl. + // The message will be the default still, so check for that, to prove it did something. + Truth.assertThat(ref.get()).isNotNull(); + Truth.assertThat(ref.get().getMessage()).contains("Error during PerfMark.<clinit>"); + } + + private static class HesitantSecurityManager extends SecurityManager { + boolean unload; + + @Override + public void checkPermission(Permission perm) { + if (unload && perm.getName().equals("setSecurityManager")) { + return; + } + if (perm instanceof FilePermission) { + FilePermission fp = (FilePermission) perm; + if ("read".equals(fp.getActions())) { + if (fp.getName().endsWith(".class") && fp.getName().contains("io/perfmark/")) { + return; + } + if (fp.getName().endsWith(".jar") && fp.getName().contains("/perfmark/")) { + return; + } + } + } + if (perm instanceof PropertyPermission) { + if (perm.getName().equals("java.util.logging.manager") && perm.getActions().equals("read")) { + return; + } + } + for (StackTraceElement element : new Throwable().getStackTrace()) { + if (element.getClassName().equals(TestClassLoader.class.getName())) { + if (perm.getName().equals("suppressAccessChecks")) { + return; + } + if (perm.getName().equals("accessSystemModules")) { + return; + } + } + if (element.getClassName().equals("java.util.logging.Level")) { + if (perm.getName().equals("suppressAccessChecks")) { + return; + } + if (perm.getName().equals("accessSystemModules")) { + return; + } + } + if (element.getClassName().equals("java.util.logging.LogManager")) { + if (perm.getName().equals("shutdownHooks")) { + return; + } + if (perm.getName().equals("setContextClassLoader")) { + return; + } + if (perm instanceof LoggingPermission && perm.getName().equals("control")) { + return; + } + } + if (element.getClassName().equals("java.util.logging.Logger")) { + if (perm.getName().equals("sun.util.logging.disableCallerCheck")) { + return; + } + if (perm.getName().equals("getClassLoader")) { + return; + } + } + } + + super.checkPermission(perm); + } + } + + @Test + public void worksWithSecurityManager_noStartEnabled_noDebug() throws Exception { + ClassLoader loader = new TestClassLoader(getClass().getClassLoader()); + + SecurityManager oldMgr = System.getSecurityManager(); + HesitantSecurityManager newMgr = new HesitantSecurityManager(); + Class<?> clz; + try { + System.setSecurityManager(newMgr); + clz = Class.forName(PerfMark.class.getName(), true, loader); + clz.getMethod("setEnabled", boolean.class).invoke(null, true); + clz.getMethod("event", String.class).invoke(null, "event"); + } finally { + newMgr.unload = true; + System.setSecurityManager(oldMgr); + } + + Class<?> storageClass = Class.forName(Storage.class.getName(), true, clz.getClassLoader()); + List<Mark> marks = (List<Mark>) storageClass.getMethod("readForTest").invoke(null); + Truth.assertThat(marks).hasSize(1); + } + + @Test + public void worksWithSecurityManager_startEnabled_noDebug() throws Exception { + ClassLoader loader = new TestClassLoader(getClass().getClassLoader()); + + SecurityManager oldMgr = System.getSecurityManager(); + HesitantSecurityManager newMgr = new HesitantSecurityManager() { + @Override + public void checkPermission(Permission perm) { + if (perm instanceof PropertyPermission) { + if (perm.getName().equals("*")) { + return; + } + if (perm.getName().equals("io.perfmark.PerfMark.startEnabled") && perm.getActions().equals("read")) { + return; + } + } + super.checkPermission(perm); + } + }; + + Class<?> clz = runWithProperty(System.getProperties(), "io.perfmark.PerfMark.startEnabled", "true", () -> { + try { + System.setSecurityManager(newMgr); + Class<?> clz2 = Class.forName(PerfMark.class.getName(), true, loader); + clz2.getMethod("event", String.class).invoke(null, "event"); + return clz2; + } finally { + newMgr.unload = true; + System.setSecurityManager(oldMgr); + } + }); + + Class<?> storageClass = Class.forName(Storage.class.getName(), true, clz.getClassLoader()); + List<Mark> marks = (List<Mark>) storageClass.getMethod("readForTest").invoke(null); + Truth.assertThat(marks).hasSize(1); + } + + @Test + public void worksWithSecurityManager_noStartEnabled_debug() throws Exception { + ClassLoader loader = new TestClassLoader(getClass().getClassLoader()); + + SecurityManager oldMgr = System.getSecurityManager(); + HesitantSecurityManager newMgr = new HesitantSecurityManager() { + @Override + public void checkPermission(Permission perm) { + if (perm instanceof PropertyPermission) { + if (perm.getName().equals("*")) { + return; + } + if (perm.getName().equals("io.perfmark.PerfMark.debug") && perm.getActions().contains("read")) { + return; + } + } + super.checkPermission(perm); + } + }; + + // TODO check logging occurred. + + Class<?> clz = runWithProperty(System.getProperties(), "io.perfmark.PerfMark.debug", "true", () -> { + try { + System.setSecurityManager(newMgr); + Class<?> clz2 = Class.forName(PerfMark.class.getName(), true, loader); + clz2.getMethod("setEnabled", boolean.class).invoke(null, true); + clz2.getMethod("event", String.class).invoke(null, "event"); + return clz2; + } finally { + newMgr.unload = true; + System.setSecurityManager(oldMgr); + } + }); + + Class<?> storageClass = Class.forName(Storage.class.getName(), true, clz.getClassLoader()); + List<Mark> marks = (List<Mark>) storageClass.getMethod("readForTest").invoke(null); + Truth.assertThat(marks).hasSize(1); + } + + @Test + public void allMethodForward_taskName() { + Storage.clearLocalStorage(); + PerfMark.setEnabled(true); + + long gen = getGen(); + + Tag tag1 = PerfMark.createTag(1); + Tag tag2 = PerfMark.createTag("two"); + Tag tag3 = PerfMark.createTag("three", 3); + PerfMark.startTask("task1", tag1); + PerfMark.startTask("task2", tag2); + PerfMark.startTask("task3", tag3); + PerfMark.startTask("task4"); + PerfMark.startTask("task5", String::valueOf); + PerfMark.attachTag(PerfMark.createTag("extra")); + PerfMark.attachTag("name", "extra2", String::valueOf); + Link link = PerfMark.linkOut(); + PerfMark.linkIn(link); + PerfMark.stopTask(); + PerfMark.stopTask("task4"); + PerfMark.stopTask("task3", tag3); + PerfMark.stopTask("task2", tag2); + PerfMark.stopTask("task1", tag1); + try (TaskCloseable task6 = PerfMark.traceTask("task6")) { + try (TaskCloseable task7 = PerfMark.traceTask("task7", String::valueOf)) {} + } + + List<Mark> marks = Storage.readForTest(); + + Truth.assertThat(marks).hasSize(24); + List<Mark> expected = + Arrays.asList( + Mark.taskStart(gen, marks.get(0).getNanoTime(), "task1"), + Mark.tag(gen, tag1.tagName, tag1.tagId), + Mark.taskStart(gen, marks.get(2).getNanoTime(), "task2"), + Mark.tag(gen, tag2.tagName, tag2.tagId), + Mark.taskStart(gen, marks.get(4).getNanoTime(), "task3"), + Mark.tag(gen, tag3.tagName, tag3.tagId), + Mark.taskStart(gen, marks.get(6).getNanoTime(), "task4"), + Mark.taskStart(gen, marks.get(7).getNanoTime(), "task5"), + Mark.tag(gen, "extra", NO_TAG_ID), + Mark.keyedTag(gen, "name", "extra2"), + Mark.link(gen, link.linkId), + Mark.link(gen, -link.linkId), + Mark.taskEnd(gen, marks.get(12).getNanoTime()), + Mark.taskEnd(gen, marks.get(13).getNanoTime(), "task4"), + Mark.tag(gen, tag3.tagName, tag3.tagId), + Mark.taskEnd(gen, marks.get(15).getNanoTime(), "task3"), + Mark.tag(gen, tag2.tagName, tag2.tagId), + Mark.taskEnd(gen, marks.get(17).getNanoTime(), "task2"), + Mark.tag(gen, tag1.tagName, tag1.tagId), + Mark.taskEnd(gen, marks.get(19).getNanoTime(), "task1"), + Mark.taskStart(gen, marks.get(20).getNanoTime(), "task6"), + Mark.taskStart(gen, marks.get(21).getNanoTime(), "task7"), + Mark.taskEnd(gen, marks.get(22).getNanoTime()), + Mark.taskEnd(gen, marks.get(23).getNanoTime())); + assertEquals(expected, marks); + } + + @Test + public void attachTag_nullFunctionFailsSilently() { + Storage.clearLocalStorage(); + PerfMark.setEnabled(true); + + PerfMark.attachTag("name", "extra2", null); + + List<Mark> marks = Storage.readForTest(); + Truth.assertThat(marks).hasSize(1); + } + + @Test + public void attachTag_functionFailureSucceeds() { + Storage.clearLocalStorage(); + PerfMark.setEnabled(true); + + PerfMark.attachTag( + "name", + "extra2", + v -> { + throw new RuntimeException("bad"); + }); + + List<Mark> marks = Storage.readForTest(); + Truth.assertThat(marks).hasSize(1); + } + + @Test + public void attachTag_functionFailureObjectFailureSucceeds() { + Storage.clearLocalStorage(); + PerfMark.setEnabled(true); + Object o = + new Object() { + @Override + public String toString() { + throw new RuntimeException("worse"); + } + }; + + PerfMark.attachTag( + "name", + o, + v -> { + throw new RuntimeException("bad"); + }); + + List<Mark> marks = Storage.readForTest(); + Truth.assertThat(marks).hasSize(1); + } + + @Test + public void attachTag_doubleFunctionFailureSucceeds() { + Storage.clearLocalStorage(); + PerfMark.setEnabled(true); + + PerfMark.attachTag( + "name", + "extra2", + v -> { + throw new RuntimeException("bad") { + @Override + public String getMessage() { + throw new RuntimeException("worse"); + } + }; + }); + + List<Mark> marks = Storage.readForTest(); + Truth.assertThat(marks).hasSize(1); + } + + public static final class FakeGenerator extends Generator { + + long generation; + + @Override + public void setGeneration(long generation) { + this.generation = generation; + } + + @Override + public long getGeneration() { + return generation; + } + } + + private static long getGen() { + try { + Class<?> implClz = Class.forName("io.perfmark.impl.SecretPerfMarkImpl$PerfMarkImpl"); + Method method = implClz.getDeclaredMethod("getGen"); + method.setAccessible(true); + return (long) method.invoke(null); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + @CanIgnoreReturnValue + private static <T> T runWithProperty(Properties properties, String name, String value, Callable<T> runnable) + throws Exception { + if (properties.containsKey(name)) { + String oldProp; + oldProp = properties.getProperty(name); + try { + System.setProperty(name, value); + return runnable.call(); + } finally{ + properties.setProperty(name, oldProp); + } + } else { + try { + System.setProperty(name, value); + return runnable.call(); + } finally{ + properties.remove(name); + } + } + } + + private static class TestClassLoader extends ClassLoader { + + private final List<String> classesToDrop; + + TestClassLoader(ClassLoader parent, String ... classesToDrop) { + super(parent); + this.classesToDrop = Arrays.asList(classesToDrop); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (classesToDrop.contains(name)) { + throw new ClassNotFoundException(); + } + if (!name.startsWith("io.perfmark.")) { + return super.loadClass(name, resolve); + } + try (InputStream is = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) { + if (is == null) { + throw new ClassNotFoundException(name); + } + byte[] data = is.readAllBytes(); + Class<?> clz = defineClass(name, data, 0, data.length); + if (resolve) { + resolveClass(clz); + } + return clz; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + private static final class TracingFilter implements Filter { + private final AtomicReference<LogRecord> ref; + + TracingFilter(Class<?> clz, AtomicReference<LogRecord> ref) { + assertEquals(PerfMark.class, clz); + this.ref = ref; + } + + @Override + public boolean isLoggable(LogRecord record) { + PerfMark.startTask("isLoggable"); + try { + ref.compareAndExchange(null, record); + return false; + } finally { + PerfMark.stopTask("isLoggable"); + } + } + } +} diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.13.37.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.13.37.jar Binary files differnew file mode 100644 index 0000000..9f2f6c0 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.13.37.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.14.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.14.0.jar Binary files differnew file mode 100644 index 0000000..5e76ef6 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.14.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.15.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.15.0.jar Binary files differnew file mode 100644 index 0000000..94f200d --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.15.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.16.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.16.0.jar Binary files differnew file mode 100644 index 0000000..1e9fe2f --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.16.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.17.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.17.0.jar Binary files differnew file mode 100644 index 0000000..8f07afa --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.17.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.19.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.19.0.jar Binary files differnew file mode 100644 index 0000000..557a056 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.19.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.20.1.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.20.1.jar Binary files differnew file mode 100644 index 0000000..be6b1bb --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.20.1.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.21.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.21.0.jar Binary files differnew file mode 100644 index 0000000..8d9fd07 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.21.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.22.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.22.0.jar Binary files differnew file mode 100644 index 0000000..fc4e3f8 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.22.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.23.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.23.0.jar Binary files differnew file mode 100644 index 0000000..690ce83 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.23.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.24.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.24.0.jar Binary files differnew file mode 100644 index 0000000..dab4c46 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.24.0.jar diff --git a/api/src/test/resources/io/perfmark/perfmark-api-0.25.0.jar b/api/src/test/resources/io/perfmark/perfmark-api-0.25.0.jar Binary files differnew file mode 100644 index 0000000..5189d23 --- /dev/null +++ b/api/src/test/resources/io/perfmark/perfmark-api-0.25.0.jar diff --git a/api/testing/build.gradle.kts b/api/testing/build.gradle.kts new file mode 100644 index 0000000..fdcfb77 --- /dev/null +++ b/api/testing/build.gradle.kts @@ -0,0 +1,24 @@ +buildscript { + extra.apply{ + set("moduleName", "io.perfmark.apitesting") + } +} + +description = "PerfMark API Tests" + +dependencies { + testImplementation(libs.truth) + + testImplementation(project(":perfmark-api")) + testImplementation(project(":perfmark-tracewriter")) + testImplementation(project(":perfmark-traceviewer")) + testImplementation(project(":perfmark-agent")) + testRuntimeOnly(project(":perfmark-java6")) + testRuntimeOnly(project(":perfmark-java7")) + testRuntimeOnly(project(":perfmark-java9")) +} + +tasks.named<JavaCompile>("compileTestJava") { + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() +} diff --git a/api/testing/src/test/java/io/perfmark/apitesting/ArtifactTest.java b/api/testing/src/test/java/io/perfmark/apitesting/ArtifactTest.java new file mode 100644 index 0000000..f6e4155 --- /dev/null +++ b/api/testing/src/test/java/io/perfmark/apitesting/ArtifactTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.apitesting; + +import static org.junit.Assert.assertNotNull; + +import com.google.common.truth.Truth; +import io.perfmark.PerfMark; +import io.perfmark.traceviewer.TraceEventViewer; +import io.perfmark.tracewriter.TraceEventWriter; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ArtifactTest { + @Test + public void hasPackage() throws Exception { + List<String> reflectiveClasses = + List.of( + "io.perfmark.impl.SecretPerfMarkImpl$PerfMarkImpl", + "io.perfmark.java9.SecretVarHandleMarkHolderProvider$VarHandleMarkHolderProvider", + "io.perfmark.java6.SecretSynchronizedMarkHolderProvider$SynchronizedMarkHolderProvider", + "io.perfmark.java7.SecretMethodHandleGenerator$MethodHandleGenerator" + ); + for (String reflectiveClass : reflectiveClasses) { + Class<?> clz = Class.forName(reflectiveClass, false, getClass().getClassLoader()); + checkPackage(clz.getPackage()); + } + checkPackage(PerfMark.class.getPackage()); + checkPackage(TraceEventWriter.class.getPackage()); + checkPackage(TraceEventViewer.class.getPackage()); + } + + private static void checkPackage(Package pkg) { + Truth.assertWithMessage(pkg.toString()).that(pkg.getImplementationTitle()).contains("PerfMark"); + + String vers = pkg.getImplementationVersion(); + assertNotNull(vers); + String[] path = vers.split("\\.", 3); + Truth.assertThat(path).hasLength(3); + Truth.assertThat(Long.parseLong(path[0])).isAtLeast(0); + Truth.assertThat(Long.parseLong(path[1])).isAtLeast(1); + + Truth.assertThat(pkg.getImplementationVendor()).isNotEmpty(); + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..143473a --- /dev/null +++ b/build.gradle @@ -0,0 +1,156 @@ +plugins { + id "net.ltgt.errorprone" version "3.0.1" apply false + id "me.champeau.jmh" version "0.6.8" apply false + id "io.github.reyerizo.gradle.jcstress" version "0.8.14" apply false +} + +subprojects { + apply plugin: "checkstyle" + apply plugin: "java-library" + apply plugin: 'maven-publish' + apply plugin: "idea" + apply plugin: "signing" + apply plugin: "net.ltgt.errorprone" + + repositories { + maven { + url "https://maven-central.storage-download.googleapis.com/repos/central/data/" } + mavenCentral() + } + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } + } + + task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc + } + + task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource + } + + checkstyle { + configDirectory = file("$rootDir/buildscripts") + toolVersion = "6.17" + ignoreFailures = false + if (rootProject.hasProperty("checkstyle.ignoreFailures")) { + ignoreFailures = rootProject.properties["checkstyle.ignoreFailures"].toBoolean() + } + } + + afterEvaluate { + jar { + manifest { + attributes ('Automatic-Module-Name': moduleName, + "Implementation-Version": archiveVersion.get(), + "Implementation-Title": "PerfMark", + "Implementation-Vendor": "Carl Mastrangelo", + "Implementation-URL": "https://www.perfmark.io/", + "Carl-Is-Awesome": "true") + } + + } + } + + + publishing { + publications { + maven(MavenPublication) { + from components.java + + artifact javadocJar + artifact sourcesJar + + pom { + name = project.group + ":" + project.name + url = 'https://github.com/perfmark/perfmark' + afterEvaluate { + // description is not available until evaluated. + description = project.description + } + + scm { + connection = 'scm:git:https://github.com/perfmark/perfmark.git' + developerConnection = 'scm:git@github.com:perfmark/perfmark.git' + url = 'https://github.com/perfmark/perfmark' + } + + licenses { + license { + name = 'Apache 2.0' + url = 'https://opensource.org/licenses/Apache-2.0' + } + } + + developers { + developer { + id = "carl-mastrangelo" + name = "Carl Mastrangelo" + email = "carl@carlmastrangelo.com" + url = "https://www.perfmark.io/" + } + } + } + } + } + + repositories { + maven { + def stagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' + def releaseUrl = stagingUrl + def snapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' + url = version.endsWith('SNAPSHOT') ? snapshotUrl : releaseUrl + credentials { + if (rootProject.hasProperty('ossrhUsername') + && rootProject.hasProperty('ossrhPassword')) { + username = rootProject.ossrhUsername + password = rootProject.ossrhPassword + } + } + } + } + } + + signing { + required false + sign publishing.publications.maven + } + + [publishMavenPublicationToMavenRepository, publishMavenPublicationToMavenLocal]*.onlyIf { + !name.contains("perfmark-examples") && !name.contains("perfmark-api-testing") + && !name.contains("perfmark-testing") && !name.contains("perfmark-agent") + } + + [javadoc]*.onlyIf { + !name.contains("perfmark-java9") && !name.contains("perfmark-examples") + && !name.contains("perfmark-api-testing") && !name.contains("perfmark-testing") + } + + if (rootProject.properties.get('errorProne', true)) { + dependencies { + errorprone 'com.google.errorprone:error_prone_core:2.16' + errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1' + } + } else { + // Disable Error Prone + allprojects { + afterEvaluate { project -> + project.tasks.withType(JavaCompile) { + options.errorprone.enabled = false + } + } + } + } + + group = "io.perfmark" + version = "0.26.0" + + dependencies { + testImplementation libs.junit + } +} diff --git a/buildscripts/checkstyle.license b/buildscripts/checkstyle.license new file mode 100644 index 0000000..38a5192 --- /dev/null +++ b/buildscripts/checkstyle.license @@ -0,0 +1,16 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + diff --git a/buildscripts/checkstyle.xml b/buildscripts/checkstyle.xml new file mode 100644 index 0000000..7d44717 --- /dev/null +++ b/buildscripts/checkstyle.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<!DOCTYPE module PUBLIC + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> + +<!-- + Checkstyle configuration that checks the Google coding conventions from Google Java Style + that can be found at https://google.github.io/styleguide/javaguide.html. + + Checkstyle is very configurable. Be sure to read the documentation at + http://checkstyle.sf.net (or in your downloaded distribution). + + To completely disable a check, just comment it out or delete it from the file. + + Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov. + --> + +<module name = "Checker"> + <property name="charset" value="UTF-8"/> + + <property name="severity" value="error"/> + + <module name="Header"> + <property name="headerFile" value="${config_loc}/checkstyle.license"/> + <property name="ignoreLines" value="2"/> + <property name="fileExtensions" value="java"/> + </module> +</module> diff --git a/doc/fix-stop-task.md b/doc/fix-stop-task.md new file mode 100644 index 0000000..d50a01c --- /dev/null +++ b/doc/fix-stop-task.md @@ -0,0 +1,166 @@ +# Unbalancing PerfMark Calls + +Tl;Dr: `PerfMark.stopTask()` will no longer use task name or tags. + +# Background + +PerfMark was designed as a replacement for an existing tracing library called +JEndoscope. JEndoscope used annotations to indicate which methods to trace. +When the classes were loaded, a Java agent would rewrite the methods to include +the tracing calls if JEndoscope was enabled. One of the benefits of this API +is that it isn't possible to forget to add the closing trace call, which +indicates a span is complete. Thus, the danger of mismatched start and stop +trace calls was not a problem. + +PerfMark was designed to not rely on an agent for tracing, and thus could not +use annotations to indicate which calls to trace. To avoid the risk for API +users, PerfMark offered matching stop trace calls that included the relevant +task name and tag. An example usage: + +```java +PerfMark.startTask("makeRPC"); +invokeSomeRPC(); +PerfMark.stopTask("makeRPC"); +``` + +When PerfMark returned the traces collected, it was expected that the +start task name matched the stop task name. It could verify these to ensure +the trace calls were properly balanced. It was expected that a warning could +be emitted to aid the user in fixing the mismatch. + +There is an additional benefit to this API that was not needed in the previous +JEndoscope model. PerfMark allows tracing to be dynamically enabled and +disable at runtime. This means that tracing may be enabled in the middle of a +start-stop pair. This would mean that only the stop would be recorded, +without knowing the starting task name. Even if the calls were properly +balanced, if only the stop edge is present, there isn't a way to reconstruct the +name of the task. Having the task name in both the start and the stop solves +this problem. (The same problem exists on the opposite side too, if the +traces are read before the task completes.) + +Finally, having the task name in both the start and stop has a symmetry to it +that made it look correct. It was clear which task was being stopped, even if +the start trace was many lines above. + +# Problems with stopTask(). + +At the time of the design, the aforementioned choices made sense. However, +several problems arose from this pattern that can't easily be worked around. + +## Stuttering Arguments Makes Code Verbose + +The PerfMark API is very fast, but it accomplishes this by trusting the +programmer to be careful. Because of this, a safe invocation of the trace +involves Wrapping the code in a try-finally block: + +```java +PerfMark.startTask("makeRPC"); +try { + invokeSomeRPC(); +} finally { + PerfMark.stopTask("makeRPC"); +} +``` + +This costs an indent block, as well as 3 extra lines per trace call. This +puts us into verbosity debt. Having multiple such calls makes the code pyramid +off the page, decreasing the readability of the code. + +In order to repay this debt, dropping the redundant task name (and tag) makes +the code less unpleasant to look at. + +While the duplicated names do have technical reasons (mentioned above), users +feedback indicated the verbosity was more of a problem. Given the relatively +rare problems with mismatched tags and split traces, addressing verbosity is +more important. + +## try-with-resources Imposes an Allocation cost + +One of the ways Java helps programmers clean up resources is the +try-with-resources idiom. PerfMark explored using this as an alternative to +bare start-stop calls with the following usage: + +```java +try (PerfMarkTask task = PerfMark.task("makeRPC")) { + invokeSomeRPC(); +} +``` + +In an ideal world, `PerfMarkTask` would be a no-op object when PerfMark is +disabled, and be a singleton object when enabled. This would in turn call the +appropriate start-stop methods. However, because the preferred start-stop calls +*both* require the task name, a new object must be allocated to capture the name +(and tag). This forces the user to choose between a runtime cost and safe +programming practice. + +## Lazy Task Names complicate API. + +Another way PerfMark explored making trace calls cheaper was to use a lambda +base task naming call. Sometimes task names are not compile time constant, and +in fact may have a significant runtime cost. Eagerly calculating these imposes +a runtime cost, even when PerfMark is disabled. The following API shows the +proposed usage: + +```java +PerfMark.startTask(rpc, RPC::nameAndId); +// Same as PerfMark.startTask(rpc.nameAndId()); +``` + +The use of a method handle avoids the unwanted calculation of the `nameAndId` +string, which the JVM may not be able to eliminate otherwise. + +The problem becomes more awkward when encouraging users to use matching names +for start and stop: + +```java +PerfMark.startTask(rpc, RPC::nameAndId); +invokeSomeRPC(); +PerfMark.stopTask(rpc, RPC::nameAndId); +``` + +Will the expensive function be calculated twice? What happens if the function +is not idempotent? The user has used the API to the best of their ability, but +the skew problems are still present. Trying to solve this is difficult without +imposing other costs, and being hard to reason about. + +### Permutations in API size. + +PerfMark has several overloads of the start - stop calls: + +* `startTask(String name);` +* `startTask(String name, Tag tag);` +* `startTask(String name, String subName);` + + +While exploring lambdas to make lazy variants of these methods, the API grew +substantially. As the number of start methods grows, so too must the stop +methods. This makes usage more error prone since there may be 10s of possible +overloads to invoke. This is an unnecessary burden on the user, since the +library already must deal with the possibility of mismatched names. + + +## Agent Byte Code is more difficult + +While JEndoscope required an agent to work, PerfMark can be optionally enhanced +by using one. It can add line and file information about the calls +when instrumenting classes. However, it's difficult to rewrite this byte code +when it depends on the possibly changing task name. JEndoscope worked around +this problem by forcing all tracing calls to be compile time constants, and +forcing rewritten calls to use the `ldc` JVM instruction. + +# Proposed Change: one stopTask() + +To avoid the problems above, a singular `stopTask()` method overload is +proposed. The existing stopTask overloads will no longer be recommended, and +behave as the single `stopTask()` method with extra info. The tracing library +will be updated to not expect this info. + +This solves each of the problems above. The usage of PerfMark becomes less +verbose. There is no longer a runtime cost to using the +try-with-resources idiom. There is no possibility of skew between start and +stop names and tags. The API will grow slightly to accommodate this new method, +but will prevent the doubling of methods. + +The benefits brought by having the name twice are seldom seen, and are paid +at runtime. The names passed to stopTask() are stored, but are never used +when visualizing trace diagrams. diff --git a/doc/perfmark.png b/doc/perfmark.png Binary files differnew file mode 100644 index 0000000..9cb68d2 --- /dev/null +++ b/doc/perfmark.png diff --git a/doc/perfmark.svg b/doc/perfmark.svg new file mode 100644 index 0000000..27df238 --- /dev/null +++ b/doc/perfmark.svg @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + version="1.1" + id="svg64" + xml:space="preserve" + width="440.94666" + height="519.77332" + viewBox="0 0 440.94666 519.77332" + style="shape-rendering:crispEdges"><defs + id="defs68" /><g + id="g72" + transform="matrix(1.3333333,0,0,-1.3333333,0,519.77333)"><g + id="g74" + transform="scale(0.1)"><path + d="m 1196.91,1582.06 c 194.03,-147.55 436.15,-235.14 698.72,-235.14 638.12,0 1155.43,517.31 1155.43,1155.43 0,638.13 -517.31,1155.44 -1155.43,1155.44 -323.86,0 -616.6,-133.25 -826.39,-347.91" + style="fill:none;stroke:#393536;stroke-width:110;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path76" + /><path + d="m 2426.78,3299.61 c 9.07,33.86 -11.03,68.66 -44.88,77.74 -33.86,9.07 -68.66,-11.02 -77.74,-44.88 -9.07,-33.86 11.03,-68.66 44.89,-77.73 33.85,-9.08 68.65,11.01 77.73,44.87" + style="fill:#393536;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path78" + /><path + d="m 2892.67,2471.47 c 9.07,33.85 -11.02,68.65 -44.87,77.73 -33.86,9.07 -68.67,-11.02 -77.74,-44.88 -9.08,-33.86 11.02,-68.66 44.87,-77.73 33.86,-9.07 68.67,11.02 77.74,44.88" + style="fill:#393536;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path80" + /><path + d="m 2768.41,2007.82 c 9.07,33.86 -11.01,68.66 -44.87,77.74 -33.86,9.08 -68.66,-11.02 -77.74,-44.88 -9.08,-33.85 11.02,-68.65 44.88,-77.73 33.85,-9.07 68.66,11.02 77.73,44.87" + style="fill:#393536;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path82" + /><path + d="m 2426.69,1672.19 c 9.07,33.86 -11.01,68.66 -44.87,77.73 -33.87,9.07 -68.66,-11.01 -77.74,-44.87 -9.08,-33.86 11.02,-68.66 44.88,-77.74 33.86,-9.07 68.65,11.02 77.73,44.88" + style="fill:#393536;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path84" + /><path + d="m 1956.89,1546.33 c 9.07,33.86 -11.02,68.66 -44.88,77.73 -33.85,9.08 -68.66,-11.01 -77.73,-44.87 -9.08,-33.86 11.02,-68.66 44.87,-77.73 33.86,-9.08 68.67,11.01 77.74,44.87" + style="fill:#393536;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path86" + /><path + d="m 1364.57,3332.52 c -9.07,-33.85 11.02,-68.66 44.88,-77.73 33.85,-9.08 68.66,11.01 77.73,44.87 9.08,33.86 -11.02,68.67 -44.87,77.74 -33.86,9.07 -68.66,-11.02 -77.74,-44.88" + style="fill:#393536;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path88" + /><path + d="m 1834.38,3458.38 c -9.07,-33.86 11.01,-68.66 44.87,-77.74 33.86,-9.07 68.66,11.02 77.74,44.88 9.07,33.86 -11.02,68.66 -44.88,77.73 -33.86,9.08 -68.66,-11.01 -77.73,-44.87" + style="fill:#393536;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path90" + /><path + d="m 1047.51,3287.03 c -3.35,-3.62 -6.68,-7.26 -9.98,-10.92" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path92" + /><path + d="m 1069.24,3309.88 c -7.35,-7.52 -14.59,-15.13 -21.73,-22.85" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path94" + /><path + d="M 1934.23,2265.15 C 1796.25,1945.29 1520.14,1698.7 1182.48,1597.86 l -1,-0.59 c -79.43,-96.51 -124.5,-220.06 -124.5,-354.82 0,-262.239 180.56,-482.22 424.12,-542.641 C 1417.4,515.648 1302.6,355.41 1153.8,235.738 925.363,510.07 787.906,862.809 787.906,1247.71 c 0,255.88 60.813,497.53 168.653,711.42" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path96" + /><path + d="m 999.555,2038.65 c -15.067,-26.04 -29.407,-52.56 -42.996,-79.52" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path98" + /><path + d="m 1462,2543.61 c 70.27,49.36 144.55,93 221.89,130.31 55.89,26.95 114.16,52.43 177.2,58.94 62.96,6.51 129.04,-13.52 196.88,-20.85 25.17,-2.72 50.87,-1.69 75.99,1.05 52.78,5.76 104.28,20.56 153.95,38.91 140.65,51.97 271.79,129.57 400.73,205.08 8.92,5.22 17.83,10.45 26.73,15.7" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path100" + /><path + d="m 689.426,3164.17 c 24.027,137.51 76.707,265.16 151.652,376.59" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path102" + /><path + d="m 1498.49,2005.92 c -60.6,-11.72 -123.18,-17.85 -187.21,-17.85 -108.97,0 -213.8,17.77 -311.725,50.58 -388.731,130.26 -668.844,497.41 -668.844,929.99 0,146.11 31.957,284.75 89.258,409.32 l 2.668,0.51 1039.363,-834.86 113.42,-91.09" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path104" + /><path + d="M 841.082,3540.76 1533.76,2591.15" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path106" + /><path + d="m 2707.1,2968.01 c -133.04,-76.35 -268.85,-153.15 -388.34,-250.05 -42.91,-34.8 -84.81,-73.88 -118.25,-118.85 -43.66,-58.74 -57.77,-132.06 -89.47,-196.85 -32.67,-66.76 -89.53,-117.17 -162.9,-134.2 -17.21,-4 -34.86,-6.02 -52.51,-6.02 -36.8,0 -71.67,8.28 -102.84,23.07" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path108" + /><path + d="m 1044.65,1586.39 c 46.37,-3.66 97.03,-0.82 137.83,11.47" + style="fill:none;stroke:#393536;stroke-width:120;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path110" + /></g></g></svg> diff --git a/doc/screenshot.png b/doc/screenshot.png Binary files differnew file mode 100644 index 0000000..4c1d52e --- /dev/null +++ b/doc/screenshot.png diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts new file mode 100644 index 0000000..44d3fa3 --- /dev/null +++ b/examples/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + application +} + +buildscript { + extra.apply { + set("moduleName", "io.perfmark.examples") + } +} + +val jdkVersion = JavaVersion.VERSION_1_8 + + +configurations { + create("perfmarkAgent") +} + +dependencies { + implementation(project(":perfmark-api")) + implementation(project(":perfmark-tracewriter")) + runtimeOnly(project(":perfmark-java7")) + runtimeOnly(project(":perfmark-java6")) + + add("perfmarkAgent", project(":perfmark-agent", configuration = "shadow")) +} + +tasks.named<JavaCompile>("compileJava") { + sourceCompatibility = jdkVersion.toString() + targetCompatibility = jdkVersion.toString() +} + +tasks.named<JavaExec>("run") { + dependsOn(":perfmark-agent:shadowJar") +} + +application { + mainClass.set("io.perfmark.examples.perfetto.WebServer") + applicationDefaultJvmArgs = mutableListOf( + "-javaagent:" + configurations.getByName("perfmarkAgent").singleFile.path, + "-Xlog:class+load=info", + "-XX:StartFlightRecording", + "-Dio.perfmark.PerfMark.startEnabled=true", + ) +} + +tasks.named<Javadoc>("javadoc") { + exclude("io/perfmark/examples/**") +} diff --git a/examples/src/main/java/io/perfmark/examples/perfetto/WebServer.java b/examples/src/main/java/io/perfmark/examples/perfetto/WebServer.java new file mode 100644 index 0000000..97f60a9 --- /dev/null +++ b/examples/src/main/java/io/perfmark/examples/perfetto/WebServer.java @@ -0,0 +1,92 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.examples.perfetto; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import io.perfmark.PerfMark; +import io.perfmark.TaskCloseable; +import io.perfmark.tracewriter.TraceEventWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +/** + * This class shows how to set up a basic HTTP server that can display PerfMark traces in the + * browser. The code here has some minor error cases ignored for the sake of brevity. + */ +public final class WebServer { + + public static void main(String[] args) throws IOException, InterruptedException { + PerfMark.setEnabled(true); + + HttpServer res = HttpServer.create(new InetSocketAddress("localhost", 0), 5); + + res.createContext("/", new IndexHandler()); + res.createContext("/trace.json", new JsonHandler()); + res.start(); + try { + InetSocketAddress listen = res.getAddress(); + System.err.println("Listening at http://localhost:" + listen.getPort() + '/'); + + while (true) { + Thread.sleep(10); + } + } finally { + res.stop(5); + } + } + + private static final class IndexHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "text/html"); + exchange.sendResponseHeaders(200, 0); + try (TaskCloseable ignored = PerfMark.traceTask("IndexHandler.handle"); + InputStream is = getClass().getResourceAsStream("index.html"); + OutputStream os = exchange.getResponseBody()) { + byte[] data = new byte[is.available()]; + int total = is.read(data); + if (total != data.length) { + throw new IOException("didn't read"); + } + os.write(data); + os.flush(); + } + } + } + + private static final class JsonHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, 0); + try (TaskCloseable ignored = PerfMark.traceTask("JsonHandler.handle"); + OutputStream os = exchange.getResponseBody(); + OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { + TraceEventWriter.writeTraceEvents(osw); + osw.flush(); + } + } + } +} diff --git a/examples/src/main/resources/io/perfmark/examples/perfetto/index.html b/examples/src/main/resources/io/perfmark/examples/perfetto/index.html new file mode 100644 index 0000000..2576d73 --- /dev/null +++ b/examples/src/main/resources/io/perfmark/examples/perfetto/index.html @@ -0,0 +1,66 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <meta name=viewport content="width=device-width, initial-scale=1"> + <title>PerfMark Trace UI</title> + <base href="/"> +</head> +<body> + <h1>PerfMark Trace Viewer</h1> + <p> + This is an example Trace Viewer, using the <a href="https://ui.perfetto.dev">Perfetto UI</a>. + </p> + <button id="thebutton">Load</button> + <br /> + <textarea id="logs" cols="80" rows="24"> + </textarea> + <script> + (function() { + let origin = "https://ui.perfetto.dev"; + let logs = document.getElementById("logs"); + let thebutton = document.getElementById("thebutton"); + let thewindow = undefined; + + thebutton.addEventListener("click", function (evt) { + logs.innerText += "Loading ...\n"; + fetch("trace.json").then(function(result) { + result.blob().then(function(blob) { + blob.arrayBuffer().then(arrayBuffer => { + logs.innerText += "Trace JSON fetched.\n"; + if (thewindow) { + thewindow.postMessage(arrayBuffer, origin); + return; + } + loadWindow(arrayBuffer); + }); + }); + }); + }); + + function loadWindow(arrayBuffer) { + if (thewindow === undefined) { + thewindow = null; + } else { + return; + } + logs.innerText += "Loading Perfetto Window\n"; + let win = window.open(origin); + let thetimer = null; + window.addEventListener("message", function(evt) { + if (evt.data !== "PONG") { + return; + } + window.clearInterval(thetimer); + logs.innerText += "Perfetto Window Ready\n"; + thewindow = win; + thewindow.postMessage(arrayBuffer, origin); + }); + thetimer = setInterval(function() { + win.postMessage("PING", origin); + }, 250); + } + })(); + </script> +</body> +</html>
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..41d9927 --- /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..ae04661 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@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 Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@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="-Xmx64m" "-Xms64m"
+
+@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 execute
+
+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 execute
+
+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
+
+: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 %*
+
+: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/impl/BUILD.bazel b/impl/BUILD.bazel new file mode 100644 index 0000000..68ba7e1 --- /dev/null +++ b/impl/BUILD.bazel @@ -0,0 +1,80 @@ +java_library( + name = "impl", + srcs = [ + "src/main/java/io/perfmark/impl/NoopGenerator.java", + "src/main/java/io/perfmark/impl/SecretPerfMarkImpl.java", + ], + deps = [ + ":generator", + ":mark", + ":storage", + "//api:impl", + "//api:link", + "//api:tag", + "//api:stringfunction", + "@maven//:com_google_code_findbugs_jsr305", + ], +) + +java_library( + name = "generator", + srcs = ["src/main/java/io/perfmark/impl/Generator.java"], + visibility = ["//:__subpackages__"], + deps = [ + "@maven//:com_google_code_findbugs_jsr305", + ], +) + +java_library( + name = "storage", + srcs = [ + "src/main/java/io/perfmark/impl/NoopMarkHolderProvider.java", + "src/main/java/io/perfmark/impl/Storage.java", + ], + visibility = ["//:__subpackages__"], + deps = [ + ":generator", + ":mark", + ":mark-holder", + ":mark-holder-provider", + ":mark-list", + "@maven//:com_google_code_findbugs_jsr305", + ], +) + +java_library( + name = "mark-holder", + srcs = ["src/main/java/io/perfmark/impl/MarkHolder.java"], + visibility = ["//:__subpackages__"], + deps = [ + ":mark", + ], +) + +java_library( + name = "mark-holder-provider", + srcs = ["src/main/java/io/perfmark/impl/MarkHolderProvider.java"], + visibility = ["//:__subpackages__"], + deps = [ + ":mark-holder", + ], +) + +java_library( + name = "mark", + srcs = ["src/main/java/io/perfmark/impl/Mark.java"], + visibility = ["//:__subpackages__"], + deps = [ + ":generator", + "@maven//:com_google_code_findbugs_jsr305", + ], +) + +java_library( + name = "mark-list", + srcs = ["src/main/java/io/perfmark/impl/MarkList.java"], + visibility = ["//:__subpackages__"], + deps = [ + ":mark", + ], +) diff --git a/impl/build.gradle b/impl/build.gradle new file mode 100644 index 0000000..7f7fddb --- /dev/null +++ b/impl/build.gradle @@ -0,0 +1,65 @@ +plugins { + id "me.champeau.jmh" +} + +description = "PerfMark Implementation API" +ext.moduleName = "io.perfmark.impl" +ext.jdkVersion = JavaVersion.VERSION_1_6 + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +compileJava { + sourceCompatibility = jdkVersion + targetCompatibility = jdkVersion + + options.compilerArgs.add("-Xlint:-options") +} + +dependencies { + implementation project(':perfmark-api') + compileOnly libs.jsr305, + libs.errorprone + testImplementation libs.truth + testCompileOnly libs.errorprone +} + + +jmh { + + timeOnIteration = "1s" + warmup = "1s" + fork = 400 + warmupIterations = 0 + + includes = ["ClassInit"] + profilers = ["cl"] + jvmArgs = ["-Dio.perfmark.PerfMark.debug=true"] + + /* + profilers = ["perfasm"] + + jvmArgs = [ + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+LogCompilation", + "-XX:LogFile=/tmp/blah.txt", + "-XX:+PrintAssembly", + "-XX:+PrintInterpreter", + "-XX:+PrintNMethods", + "-XX:+PrintNativeNMethods", + "-XX:+PrintSignatureHandlers", + "-XX:+PrintAdapterHandlers", + "-XX:+PrintStubCode", + "-XX:+PrintCompilation", + "-XX:+PrintInlining", + "-XX:+TraceClassLoading", + "-XX:PrintAssemblyOptions=syntax", + "-XX:PrintAssemblyOptions=intel" + ] + */ + + //duplicateClassesStrategy DuplicatesStrategy.INCLUDE +}
\ No newline at end of file diff --git a/impl/src/jmh/java/io/perfmark/impl/SecretPerfMarkImplClassInitBenchmark.java b/impl/src/jmh/java/io/perfmark/impl/SecretPerfMarkImplClassInitBenchmark.java new file mode 100644 index 0000000..77e9786 --- /dev/null +++ b/impl/src/jmh/java/io/perfmark/impl/SecretPerfMarkImplClassInitBenchmark.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +@Fork(1000) +@Warmup(iterations = 0) +@Measurement(iterations = 1) +public class SecretPerfMarkImplClassInitBenchmark { + + private ClassLoader loader; + + @Setup + public void setup() { + loader = + new TestClassLoader(getClass().getClassLoader(), + "io.perfmark.java7.SecretMethodHandleGenerator$MethodHandleGenerator", + "io.perfmark.java9.SecretVarHandleGenerator$VarHandleGenerator", + "io.perfmark.java6.SecretVolatileGenerator$VolatileGenerator"); + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public Object forName_noinit() throws Exception { + return Class.forName(SecretPerfMarkImpl.PerfMarkImpl.class.getName(), false, loader); + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public Object forName_init() throws Exception { + return Class.forName(SecretPerfMarkImpl.PerfMarkImpl.class.getName(), true, loader); + } + + private static class TestClassLoader extends ClassLoader { + + private final List<String> classesToDrop; + + TestClassLoader(ClassLoader parent, String ... classesToDrop) { + super(parent); + this.classesToDrop = Arrays.asList(classesToDrop); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (classesToDrop.contains(name)) { + // Throw an exception, just like the real one would. + throw new ClassNotFoundException(); + } + if (!name.startsWith("io.perfmark.")) { + return super.loadClass(name, resolve); + } + try (InputStream is = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) { + if (is == null) { + throw new ClassNotFoundException(name); + } + byte[] data = is.readAllBytes(); + Class<?> clz = defineClass(name, data, 0, data.length); + if (resolve) { + resolveClass(clz); + } + return clz; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/impl/src/main/java/io/perfmark/impl/Generator.java b/impl/src/main/java/io/perfmark/impl/Generator.java new file mode 100644 index 0000000..f2a717e --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/Generator.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +/** + * A Generator keeps track of what generation the PerfMark library is on. A generation is a way to + * group PerfMark recorded tasks and links together. This allows dynamically enabling and disabling + * PerfMark. Each time the library is enabled, a new generation is started and associated with all + * the recorded data. + * + * <p>This class is not threadsafe. Synchronization is handled externally. + * + * <p>Normal users are not expected to use this class. + */ +public abstract class Generator { + /** + * This field is here as a hack. This class is a shared dependency of both {@link + * SecretPerfMarkImpl} and {@link Storage}. The impl needs to record the first time an event + * occurs, but doesn't call Storage#clinit until PerfMark is enabled. This leads to the timings + * being off in the trace event viewer, since the "start" time is since it was enabled, rather + * than when the first PerfMark call happens. + */ + static final long INIT_NANO_TIME = System.nanoTime(); + + /** + * This field is also here as a hack, capturing the time the program stated. + */ + static final long INIT_CURRENT_TIME_MILLIS = System.currentTimeMillis(); + + /** + * The number of reserved bits at the bottom of the generation. All generations should be + * left-shifted by this amount. + */ + public static final int GEN_OFFSET = 8; + /** + * Represents a failure to enable PerfMark library. This can be used by subclasses to indicate a + * previous failure to set the generation. It can also be used to indicate the generation count + * has overflowed. + */ + public static final long FAILURE = -2L << GEN_OFFSET; + + protected Generator() {} + + /** + * Sets the current generation count. This should be eventually noticeable for callers of {@link + * #getGeneration()}. An odd number means the library is enabled, while an even number means the + * library is disabled. + * + * @param generation the generation id, shifted left by {@link #GEN_OFFSET}. + */ + public abstract void setGeneration(long generation); + + /** + * Gets the current generation, shifted left by {@link #GEN_OFFSET}. An odd number means the + * library is enabled, while an even number means the library is disabled. + * + * @return the current generation or {@link #FAILURE}. + */ + public abstract long getGeneration(); + + /** + * Returns the approximate cost to change the generation. + * + * @return an approximate number of nanoseconds needed to change the generator value. + */ + public long costOfSetNanos() { + return 1000000; + } + + /** + * Returns the approximate cost to read the generation. + * + * @return an approximate number of nanoseconds needed to read the generator value. + */ + public long costOfGetNanos() { + return 10; + } +} diff --git a/impl/src/main/java/io/perfmark/impl/LocalMarkHolder.java b/impl/src/main/java/io/perfmark/impl/LocalMarkHolder.java new file mode 100644 index 0000000..ecfd0b4 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/LocalMarkHolder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Carl Mastrangelo + * + * 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 io.perfmark.impl; + +/** + * A local MarkHolder is a class that gets the "current" MarkHolder based on context. For example, a thread local + * MarkHolder could use this class to pull the local MarkHolder from the threadlocal variable. Other implementations + * are possible as well. + */ +public abstract class LocalMarkHolder { + + /** + * Removes the local markholder storage. + */ + public abstract void clear(); + + /** + * Get's the current MarkHolder for mutation. Only called from a tracing thread. + */ + public abstract MarkHolder acquire(); + + /** + * Releases the MarkHolder from being written too. Usually called very shortly after {@link #acquire()}. + * This method is meant to be overridden and should not be called from subclasses. + */ + public void release(MarkHolder markHolder) {} +} diff --git a/impl/src/main/java/io/perfmark/impl/Mark.java b/impl/src/main/java/io/perfmark/impl/Mark.java new file mode 100644 index 0000000..a07c194 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/Mark.java @@ -0,0 +1,505 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +import java.util.Arrays; +import javax.annotation.Nullable; + +public final class Mark { + // TODO: make sure these match the values in Impl + public static final String NO_TAG_NAME = ""; + public static final long NO_TAG_ID = Long.MIN_VALUE; + public static final long NO_LINK_ID = Long.MIN_VALUE; + + public static final long NO_NANOTIME = 0; + + private static final long N0 = 0; + private static final String S0 = null; + + private final long generation; + + private final long n1; + private final long n2; + private final long n3; + + @Nullable private final String s1; + @Nullable private final String s2; + @Nullable private final String s3; + + private final Operation operation; + + public static Mark taskStart(long generation, long nanoTime, String name) { + return new Mark(nanoTime, N0, N0, name, S0, S0, generation, Operation.TASK_START_N1S1); + } + + public static Mark taskStart(long generation, long nanoTime, String name, String subName) { + return new Mark(nanoTime, N0, N0, name, subName, S0, generation, Operation.TASK_START_N1S2); + } + + public static Mark taskEnd(long generation, long nanoTime) { + return new Mark(nanoTime, N0, N0, S0, S0, S0, generation, Operation.TASK_END_N1S0); + } + + public static Mark taskEnd(long generation, long nanoTime, String name) { + return new Mark(nanoTime, N0, N0, name, S0, S0, generation, Operation.TASK_END_N1S1); + } + + public static Mark taskEnd(long generation, long nanoTime, String name, String subName) { + return new Mark(nanoTime, N0, N0, name, subName, S0, generation, Operation.TASK_END_N1S2); + } + + public static Mark event(long generation, long nanoTime, String name) { + return new Mark(nanoTime, N0, N0, name, S0, S0, generation, Operation.EVENT_N1S1); + } + + public static Mark event(long generation, long nanoTime, String name, String subName) { + return new Mark(nanoTime, N0, N0, name, subName, S0, generation, Operation.EVENT_N1S2); + } + + public static Mark event( + long generation, long nanoTime, String taskName, String tagName, long tagId) { + return new Mark( + nanoTime, tagId, N0, taskName, tagName, S0, generation, Operation.EVENT_N2S2); + } + + public static Mark event( + long generation, + long nanoTime, + String taskName, + String subTaskName, + String tagName, + long tagId) { + return new Mark( + nanoTime, tagId, N0, taskName, subTaskName, tagName, generation, Operation.EVENT_N2S3); + } + + public static Mark tag(long generation, String tagName, long tagId) { + return new Mark(tagId, N0, N0, tagName, S0, S0, generation, Operation.TAG_N1S1); + } + + public static Mark tag(long generation, long tagId) { + return new Mark(tagId, N0, N0, S0, S0, S0, generation, Operation.TAG_N1S0); + } + + public static Mark tag(long generation, String tagName) { + return new Mark(N0, N0, N0, tagName, S0, S0, generation, Operation.TAG_N0S1); + } + + public static Mark keyedTag(long generation, String tagName, String value) { + return new Mark(N0, N0, N0, tagName, value, S0, generation, Operation.TAG_KEYED_N0S2); + } + + public static Mark keyedTag(long generation, String tagName, long value) { + return new Mark(value, N0, N0, tagName, S0, S0, generation, Operation.TAG_KEYED_N1S1); + } + + public static Mark keyedTag(long generation, String tagName, long value0, long value1) { + return new Mark(value0, value1, N0, tagName, S0, S0, generation, Operation.TAG_KEYED_N2S1); + } + + public static Mark link(long generation, long linkId) { + return new Mark(linkId, N0, N0, S0, S0, S0, generation, Operation.LINK); + } + + public Mark withTaskName(String name) { + switch (operation) { + case EVENT_N1S1: + case TASK_END_N1S1: + case TASK_START_N1S1: + return new Mark(n1, n2, n3, name, s2, s3, generation, operation); + case TASK_START_N1S2: + case TASK_END_N1S0: + case NONE: + case TASK_END_N1S2: + case EVENT_N1S2: + case EVENT_N2S2: + case EVENT_N2S3: + case LINK: + case TAG_N0S1: + case TAG_N1S0: + case TAG_N1S1: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + case TAG_KEYED_N0S2: + throw new UnsupportedOperationException(); + } + throw new AssertionError(); + } + + private Mark( + long n1, + long n2, + long n3, + @Nullable String s1, + @Nullable String s2, + @Nullable String s3, + long generation, + Operation operation) { + if (operation == null) { + throw new NullPointerException("operation"); + } + this.operation = operation; + this.generation = generation; + + if (operation == Operation.NONE) { + throw new IllegalArgumentException("bad operation"); + } + this.n1 = n1; + this.n2 = n2; + this.n3 = n3; + + this.s1 = s1; + this.s2 = s2; + this.s3 = s3; + } + + public enum OperationType { + NONE, + TASK_START, + TASK_END, + EVENT, + LINK, + TAG, + ; + } + + public enum Operation { + NONE(OperationType.NONE, 0, 0), + /** startTask(String taskName) 1 long for nanoTime. */ + TASK_START_N1S1(OperationType.TASK_START, 1, 1), + /** startTask(String name, String subTaskName) 1 long for nanoTime. */ + TASK_START_N1S2(OperationType.TASK_START, 1, 2), + + TASK_END_N1S0(OperationType.TASK_END, 1, 0), + TASK_END_N1S1(OperationType.TASK_END, 1, 1), + TASK_END_N1S2(OperationType.TASK_END, 1, 2), + + EVENT_N1S1(OperationType.EVENT, 1, 1), + EVENT_N1S2(OperationType.EVENT, 1, 2), + /** Tagged event, since attach tags can't apply to events */ + EVENT_N2S2(OperationType.EVENT, 2, 2), + /** Tagged event, since attach tags can't apply to events */ + EVENT_N2S3(OperationType.EVENT, 2, 3), + + LINK(OperationType.LINK, 1, 0), + + /** An unkeyed tag that has a single string value. */ + TAG_N0S1(OperationType.TAG, 0, 1), + /** An unkeyed tag that has a single numeric value. */ + TAG_N1S0(OperationType.TAG, 1, 0), + /** + * An unkeyed tag that has a string and numeric value. The values are unrelated to each other. + */ + TAG_N1S1(OperationType.TAG, 1, 1), + + TAG_KEYED_N1S1(OperationType.TAG, 1, 1), + + TAG_KEYED_N2S1(OperationType.TAG, 2, 1), + + TAG_KEYED_N0S2(OperationType.TAG, 0, 2), + ; + + private final OperationType opType; + private final int longs; + private final int strings; + + Operation(OperationType opType, int longs, int strings) { + this.opType = opType; + this.longs = longs; + this.strings = strings; + assert longs <= maxNumbers(); + assert strings <= maxStrings(); + } + + private static final Operation[] values = Operation.values(); + + static { + assert values.length <= (1 << Generator.GEN_OFFSET); + } + + public OperationType getOpType() { + return opType; + } + + public int getNumbers() { + return longs; + } + + public int getStrings() { + return strings; + } + + public static int maxNumbers() { + return 2; + } + + public static int maxStrings() { + return 3; + } + + public static int maxMarkers() { + return 1; + } + + public static Operation valueOf(int code) { + return values[code]; + } + } + + public long getNanoTime() { + switch (operation.opType) { + case TASK_START: + case TASK_END: + case EVENT: + return n1; + case NONE: + case LINK: + case TAG: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation.opType); + } + + public long getGeneration() { + return generation; + } + + public Operation getOperation() { + return operation; + } + + public String getTagStringValue() { + switch (operation) { + case TAG_N0S1: + case TAG_N1S1: + return s1; + case TAG_KEYED_N0S2: + case EVENT_N2S2: + return s2; + case EVENT_N2S3: + return s3; + case TAG_N1S0: + case NONE: + case TASK_START_N1S1: + case TASK_START_N1S2: + case TASK_END_N1S0: + case TASK_END_N1S1: + case TASK_END_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + case LINK: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation.opType); + } + + public long getTagFirstNumeric() { + switch (operation) { + case TAG_N1S0: + case TAG_N1S1: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + return n1; + case EVENT_N2S2: + case EVENT_N2S3: + return n2; + case TAG_N0S1: + case TAG_KEYED_N0S2: + case NONE: + case TASK_START_N1S1: + case TASK_START_N1S2: + case TASK_END_N1S0: + case TASK_END_N1S1: + case TASK_END_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case LINK: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation.opType); + } + + public long getTagSecondNumeric() { + switch (operation) { + case TAG_KEYED_N2S1: + return n2; + case TAG_N1S0: + case TAG_N1S1: + case TAG_KEYED_N1S1: + case EVENT_N2S2: + case EVENT_N2S3: + case TAG_N0S1: + case TAG_KEYED_N0S2: + case NONE: + case TASK_START_N1S1: + case TASK_START_N1S2: + case TASK_END_N1S0: + case TASK_END_N1S1: + case TASK_END_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case LINK: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation.opType); + } + + public String getTagKey() { + switch (operation) { + case TAG_KEYED_N0S2: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + return s1; + case TAG_N1S1: + case TAG_N0S1: + case TAG_N1S0: + case NONE: + case TASK_START_N1S1: + case TASK_START_N1S2: + case TASK_END_N1S0: + case TASK_END_N1S1: + case TASK_END_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case EVENT_N2S2: + case EVENT_N2S3: + case LINK: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation.opType); + } + + public String getTaskName() { + switch (operation) { + case TASK_START_N1S1: + case TASK_START_N1S2: + case TASK_END_N1S1: + case TASK_END_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case EVENT_N2S2: + case EVENT_N2S3: + return s1; + case NONE: + case LINK: + case TASK_END_N1S0: + case TAG_N0S1: + case TAG_N1S0: + case TAG_N1S1: + case TAG_KEYED_N0S2: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation); + } + + public String getSubTaskName() { + switch (operation) { + case TASK_END_N1S2: + case TASK_START_N1S2: + case EVENT_N1S2: + case EVENT_N2S3: + return s2; + case TASK_START_N1S1: + case TASK_END_N1S0: + case TASK_END_N1S1: + case EVENT_N1S1: + case EVENT_N2S2: + case NONE: + case LINK: + case TAG_N0S1: + case TAG_KEYED_N0S2: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + case TAG_N1S0: + case TAG_N1S1: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation); + } + + public long getLinkId() { + switch (operation.opType) { + case LINK: + return n1; + case TASK_START: + case TASK_END: + case EVENT: + case NONE: + case TAG: + throw new UnsupportedOperationException(); + } + throw new AssertionError(operation.opType); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Mark)) { + return false; + } + Mark that = (Mark) obj; + return equal(this.s1, that.s1) + && equal(this.s2, that.s2) + && equal(this.s3, that.s3) + && this.n1 == that.n1 + && this.n2 == that.n2 + && this.n3 == that.n3 + && this.operation == that.operation + && this.generation == that.generation; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {operation, s1, s2, s3, n1, n2, n3, generation}); + } + + @Override + public String toString() { + return "Mark{" + + "operation=" + + operation + + ", " + + "s1=" + + s1 + + ", " + + "s2=" + + s2 + + ", " + + "s3=" + + s3 + + ", " + + "n1=" + + n1 + + ", " + + "n2=" + + n2 + + ", " + + "n3=" + + n3 + + ", " + + "generation=" + + generation + + "}"; + } + + static <T> boolean equal(T a, T b) { + return a == b || a.equals(b); + } +} diff --git a/impl/src/main/java/io/perfmark/impl/MarkHolder.java b/impl/src/main/java/io/perfmark/impl/MarkHolder.java new file mode 100644 index 0000000..a18fdfd --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/MarkHolder.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +import java.util.List; + +public abstract class MarkHolder { + + public static final int NO_MAX_MARKS = -1; + + public abstract void start(long gen, String taskName, String tagName, long tagId, long nanoTime); + + public abstract void start(long gen, String taskName, long nanoTime); + + public abstract void start(long gen, String taskName, String subTaskName, long nanoTime); + + public abstract void link(long gen, long linkId); + + public abstract void stop(long gen, long nanoTime); + + public abstract void stop(long gen, String taskName, String tagName, long tagId, long nanoTime); + + public abstract void stop(long gen, String taskName, long nanoTime); + + public abstract void stop(long gen, String taskName, String subTaskName, long nanoTime); + + public abstract void event(long gen, String eventName, String tagName, long tagId, long nanoTime); + + public abstract void event(long gen, String eventName, long nanoTime); + + public abstract void event(long gen, String eventName, String subEventName, long nanoTime); + + public abstract void attachTag(long gen, String tagName, long tagId); + + public abstract void attachKeyedTag(long gen, String name, String value); + + public abstract void attachKeyedTag(long gen, String name, long value0); + + public abstract void attachKeyedTag(long gen, String name, long value0, long value1); + + public abstract void resetForTest(); + + public abstract List<Mark> read(boolean concurrentWrites); + + public int maxMarks() { + return NO_MAX_MARKS; + } + + protected MarkHolder() {} +} diff --git a/impl/src/main/java/io/perfmark/impl/MarkHolderProvider.java b/impl/src/main/java/io/perfmark/impl/MarkHolderProvider.java new file mode 100644 index 0000000..be11451 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/MarkHolderProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +public abstract class MarkHolderProvider { + + protected MarkHolderProvider() {} + + /** + * To be removed in 0.26.0 + * + * @return the new MarkHolder for the current thread. + */ + @Deprecated + public MarkHolder create() { + throw new UnsupportedOperationException(); + } + + /** + * Creates a new MarkHolder. Mark holders are always mutated by the thread that created them, (e.g. THIS thread), + * but may be read by other threads. + * + * @param markHolderId the Unique ID associated with the Mark Holder. This exists as a work around to Java's + * thread ID, which does not guarantee they will not be reused. + * @return the new MarkHolder for the current thread. + * @since 0.24.0 + */ + public MarkHolder create(long markHolderId) { + return create(); + } +} diff --git a/impl/src/main/java/io/perfmark/impl/MarkList.java b/impl/src/main/java/io/perfmark/impl/MarkList.java new file mode 100644 index 0000000..dc195de --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/MarkList.java @@ -0,0 +1,196 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; + +/** MarkList is collection of Marks, in the order they were recorded. */ +public final class MarkList extends AbstractList<Mark> { + public static Builder newBuilder() { + return new Builder(); + } + + private final List<Mark> marks; + private final long threadId; + private final long markListId; + private final String threadName; + + MarkList(Builder builder) { + if (builder.marks == null) { + throw new NullPointerException("marks"); + } + this.marks = builder.marks; + if (builder.threadName == null) { + throw new NullPointerException("threadName"); + } + this.threadName = builder.threadName; + this.threadId = builder.threadId; + this.markListId = builder.markListId; + } + + /** + * Gets the Thread name of the thread that recorded the Marks. + * + * @return the Thread name. + */ + public String getThreadName() { + return threadName; + } + + /** + * Thread IDs can be recycled, so this is not unique. + * + * @return the id of the thread, as returned by {@link Thread#getId()}. + */ + public long getThreadId() { + return threadId; + } + + /** + * The globally unique ID for this Mark list. Unlike {@link #getThreadId()}, this value is never + * recycled. + * + * @return the id of this list. + */ + public long getMarkListId() { + return markListId; + } + + @Override + public Mark get(int index) { + return marks.get(index); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MarkList)) { + return false; + } + MarkList that = (MarkList) obj; + return Mark.equal(this.marks, that.marks) + && this.threadId == that.threadId + && this.markListId == that.markListId + && Mark.equal(this.threadName, that.threadName); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {marks, threadId, markListId, threadName}); + } + + @Override + public int size() { + return marks.size(); + } + + @Override + public String toString() { + return "MarkList{" + + "marks=" + + marks + + ", " + + "threadId=" + + threadId + + ", " + + "markListId=" + + markListId + + ", " + + "threadName=" + + threadName + + "}"; + } + + public Builder toBuilder() { + Builder builder = newBuilder(); + builder.marks = marks; + return builder.setThreadName(threadName).setThreadId(threadId).setMarkListId(markListId); + } + + public static final class Builder { + + List<Mark> marks; + String threadName; + long threadId; + long markListId; + + public MarkList build() { + return new MarkList(this); + } + + /** + * Sets the marks for this MarkList builder. + * + * @throws NullPointerException if any element in this list is {@code null}. + * @param marks the marks to set. + * @return this + */ + public Builder setMarks(List<Mark> marks) { + if (marks == null) { + throw new NullPointerException("marks"); + } + ArrayList<Mark> copy = new ArrayList<Mark>(marks.size()); + ListIterator<Mark> it = marks.listIterator(); + while (it.hasNext()) { + Mark mark = it.next(); + if (mark == null) { + throw new NullPointerException("mark is null at pos " + (it.nextIndex() - 1)); + } + copy.add(mark); + } + this.marks = Collections.unmodifiableList(copy); + return this; + } + + /** + * Sets the thread name for this MarkList builder. + * + * @param threadName the Thread Name + * @return this + */ + public Builder setThreadName(String threadName) { + this.threadName = threadName; + return this; + } + + /** + * Sets the thread ID for this MarkList builder. + * + * @param threadId the Thread Id + * @return this + */ + public Builder setThreadId(long threadId) { + this.threadId = threadId; + return this; + } + + /** + * Sets the mark list ID for this MarkList builder. + * + * @param markListId the mark list ID + * @return this + */ + public Builder setMarkListId(long markListId) { + this.markListId = markListId; + return this; + } + } +} diff --git a/impl/src/main/java/io/perfmark/impl/MostlyThreadLocal.java b/impl/src/main/java/io/perfmark/impl/MostlyThreadLocal.java new file mode 100644 index 0000000..1c72cfa --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/MostlyThreadLocal.java @@ -0,0 +1,155 @@ +/* + * Copyright 2022 Carl Mastrangelo + * + * 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 io.perfmark.impl; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * This class tries to access the mark holder in a thread local. If the class is unable to store the created + * MarkHolder in the thread local, a concurrent map of Thread to MarkHolders is used for lookup. In Java 19 + * Thread Locals can be disabled due to use of Virtual threads. + * + * <p> + * Note that this class avoids using {@link #initialValue()} because it may be called multiple times, in the + * case that ThreadLocals are unsettable. + */ +final class MostlyThreadLocal extends ThreadLocal<MarkHolder> { + private static final int BITS = 11; + private static final int SIZE = 1 << BITS; + private static final int MASK = SIZE - 1; + private static final AtomicReferenceArray<CopyOnWriteArrayList<Storage.MarkHolderHandle>> threadToHandles = + new AtomicReferenceArray<CopyOnWriteArrayList<Storage.MarkHolderHandle>>(SIZE); + + MostlyThreadLocal() {} + + @Override + public MarkHolder get() { + MarkHolder markHolder = super.get(); + if (markHolder != null) { + return markHolder; + } + return getAndSetSlow(); + } + + private MarkHolder getAndSetSlow() { + assert super.get() == null; + Storage.MarkHolderHandle markHolderHandle; + MarkHolder markHolder; + Storage.MarkHolderAndHandle markHolderAndHandle; + Thread thread = Thread.currentThread(); + CopyOnWriteArrayList<Storage.MarkHolderHandle> handles = getHandles(thread); + if (handles == null) { + markHolderAndHandle = Storage.allocateMarkHolder(); + markHolder = markHolderAndHandle.markHolder(); + assert markHolder != null; + markHolderHandle = markHolderAndHandle.handle(); + try { + set(markHolder); + return markHolder; + } catch (UnsupportedOperationException e) { + // ignore. + } + handles = getOrCreateHandles(thread); + } else { + if ((markHolderHandle = getConcurrent(handles)) != null) { + if ((markHolder = markHolderHandle.markHolder()) != null) { + return markHolder; + } + } + markHolderAndHandle = Storage.allocateMarkHolder(); + markHolder = markHolderAndHandle.markHolder(); + assert markHolder != null; + markHolderHandle = markHolderAndHandle.handle(); + } + handles.add(markHolderHandle); + return markHolder; + } + + @Override + public void remove() { + Thread thread = Thread.currentThread(); + CopyOnWriteArrayList<Storage.MarkHolderHandle> handles = getHandles(thread); + if (handles == null) { + super.remove(); + } else { + assert super.get() == null; + for (Storage.MarkHolderHandle handle : handles) { + Thread t = handle.threadRef().get(); + if (t == null || t == thread) { + handles.remove(handle); + } + } + } + } + + /** + * Returns the handles for the given thread index bucket, or {@code null}. + */ + private static CopyOnWriteArrayList<Storage.MarkHolderHandle> getHandles(Thread thread) { + int hashCode = System.identityHashCode(thread); + int index = hashCode & MASK; + return threadToHandles.get(index); + } + + private static CopyOnWriteArrayList<Storage.MarkHolderHandle> getOrCreateHandles(Thread thread) { + int hashCode = System.identityHashCode(thread); + int index = hashCode & MASK; + CopyOnWriteArrayList<Storage.MarkHolderHandle> handles; + do { + if ((handles = threadToHandles.get(index)) != null) { + break; + } + } while ( + !threadToHandles.compareAndSet(index, null, (handles = new CopyOnWriteArrayList<Storage.MarkHolderHandle>()))); + return handles; + } + + /** + * May return {@code null} if not found. + */ + private static Storage.MarkHolderHandle getConcurrent(CopyOnWriteArrayList<Storage.MarkHolderHandle> handles) { + if (!handles.isEmpty()) { + Storage.MarkHolderHandle handle; + try { + handle = handles.get(0); + } catch (IndexOutOfBoundsException e) { + return null; + } + if (handle.threadRef().get() == Thread.currentThread()) { + return handle; + } + return slowGetConcurrent(handles); + } + return null; + } + + /** + * May return {@code null} if not found. + */ + private static Storage.MarkHolderHandle slowGetConcurrent(CopyOnWriteArrayList<Storage.MarkHolderHandle> handles) { + for (Storage.MarkHolderHandle handle : handles) { + Thread thread = handle.threadRef().get(); + if (thread == null) { + handles.remove(handle); + } else if (thread == Thread.currentThread()) { + return handle; + } + } + return null; + } +} diff --git a/impl/src/main/java/io/perfmark/impl/MostlyThreadLocalMarkHolder.java b/impl/src/main/java/io/perfmark/impl/MostlyThreadLocalMarkHolder.java new file mode 100644 index 0000000..a17c8c0 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/MostlyThreadLocalMarkHolder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 Carl Mastrangelo + * + * 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 io.perfmark.impl; + +final class MostlyThreadLocalMarkHolder extends LocalMarkHolder { + + private static final MostlyThreadLocal localMarkHolder = new MostlyThreadLocal(); + + MostlyThreadLocalMarkHolder() {} + + @Override + public MarkHolder acquire() { + return localMarkHolder.get(); + } + + @Override + public void release(MarkHolder markHolder) {} + + @Override + public void clear() { + localMarkHolder.remove(); + } +} diff --git a/impl/src/main/java/io/perfmark/impl/NoopGenerator.java b/impl/src/main/java/io/perfmark/impl/NoopGenerator.java new file mode 100644 index 0000000..d28089d --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/NoopGenerator.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +/** Noop Generator for use when no other generator can be used. */ +final class NoopGenerator extends Generator { + + NoopGenerator() {} + + @Override + public void setGeneration(long generation) {} + + @Override + public long getGeneration() { + return FAILURE; + } +} diff --git a/impl/src/main/java/io/perfmark/impl/NoopMarkHolderProvider.java b/impl/src/main/java/io/perfmark/impl/NoopMarkHolderProvider.java new file mode 100644 index 0000000..aa44356 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/NoopMarkHolderProvider.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +import java.util.Collections; +import java.util.List; + +final class NoopMarkHolderProvider extends MarkHolderProvider { + NoopMarkHolderProvider() {} + + @Override + @SuppressWarnings("deprecation") + public MarkHolder create() { + return new NoopMarkHolder(); + } + + @Override + public MarkHolder create(long markHolderId) { + return new NoopMarkHolder(); + } + + + private static final class NoopMarkHolder extends MarkHolder { + + NoopMarkHolder() {} + + @Override + public void start(long gen, String taskName, String tagName, long tagId, long nanoTime) {} + + @Override + public void start(long gen, String taskName, long nanoTime) {} + + @Override + public void start(long gen, String taskName, String subTaskName, long nanoTime) {} + + @Override + public void link(long gen, long linkId) {} + + @Override + public void stop(long gen, long nanoTime) {} + + @Override + public void stop(long gen, String taskName, String tagName, long tagId, long nanoTime) {} + + @Override + public void stop(long gen, String taskName, long nanoTime) {} + + @Override + public void stop(long gen, String taskName, String subTaskName, long nanoTime) {} + + @Override + public void event(long gen, String eventName, String tagName, long tagId, long nanoTime) {} + + @Override + public void event(long gen, String eventName, long nanoTime) {} + + @Override + public void event(long gen, String eventName, String subEventName, long nanoTime) {} + + @Override + public void attachTag(long gen, String tagName, long tagId) {} + + @Override + public void attachKeyedTag(long gen, String name, String value) {} + + @Override + public void attachKeyedTag(long gen, String name, long value0) {} + + @Override + public void attachKeyedTag(long gen, String name, long value0, long value1) {} + + @Override + public void resetForTest() {} + + @Override + public List<Mark> read(boolean readerIsWriter) { + return Collections.emptyList(); + } + } +} diff --git a/impl/src/main/java/io/perfmark/impl/SecretPerfMarkImpl.java b/impl/src/main/java/io/perfmark/impl/SecretPerfMarkImpl.java new file mode 100644 index 0000000..0f11a72 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/SecretPerfMarkImpl.java @@ -0,0 +1,443 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +import io.perfmark.Impl; +import io.perfmark.Link; +import io.perfmark.StringFunction; +import io.perfmark.Tag; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +final class SecretPerfMarkImpl { + + public static final class PerfMarkImpl extends Impl { + private static final int ENABLED_BIT_SPACE = 2; + private static final int GEN_TIMESTAMP_SPACE = 54; + private static final long MAX_MIBROS = (1L << GEN_TIMESTAMP_SPACE) - 1; + private static final Tag NO_TAG = packTag(Mark.NO_TAG_NAME, Mark.NO_TAG_ID); + private static final Link NO_LINK = packLink(Mark.NO_LINK_ID); + private static final long INCREMENT = 1L << Generator.GEN_OFFSET; + + private static final AtomicLong linkIdAlloc = new AtomicLong(1); + private static final Generator generator; + + // May be null if debugging is disabled. + private static final Object logger; + + /** + * This is the generation of the recorded tasks. The bottom 8 bits [0-7] are reserved for opcode packing. + * Bit 9 [8] is used for detecting if PerfMark is enabled or not. Bit 10 [9] is unused. Bits 11-64 [10-647] + * are used for storing the time since Perfmark Was last / enabled or disabled. The units are in nanoseconds/1024, + * or (inaccurately) called mibros (like micros, but power of 2 based). + */ + private static long actualGeneration; + + static { + assert ENABLED_BIT_SPACE + Generator.GEN_OFFSET + GEN_TIMESTAMP_SPACE <= 64; + Generator gen = null; + Throwable[] problems = new Throwable[4]; + // Avoid using a for-loop for this code, as it makes it easier for tools like Proguard to rewrite. + try { + Class<?> clz = Class.forName("io.perfmark.java7.SecretMethodHandleGenerator$MethodHandleGenerator"); + gen = clz.asSubclass(Generator.class).getConstructor().newInstance(); + } catch (Throwable t) { + problems[0] = t; + } + if (gen == null) { + try { + Class<?> clz = Class.forName("io.perfmark.java9.SecretVarHandleGenerator$VarHandleGenerator"); + gen = clz.asSubclass(Generator.class).getConstructor().newInstance(); + } catch (Throwable t) { + problems[1] = t; + } + } + if (gen == null) { + try { + Class<?> clz = Class.forName("io.perfmark.java6.SecretVolatileGenerator$VolatileGenerator"); + gen = clz.asSubclass(Generator.class).getConstructor().newInstance(); + } catch (Throwable t) { + problems[2] = t; + } + } + if (gen == null) { + generator = new NoopGenerator(); + } else { + generator = gen; + } + + boolean startEnabled = false; + boolean startEnabledSuccess = false; + try { + if ((startEnabled = Boolean.getBoolean("io.perfmark.PerfMark.startEnabled"))) { + startEnabledSuccess = setEnabledQuiet(startEnabled, Generator.INIT_NANO_TIME); + } + } catch (Throwable t) { + problems[3] = t; + } + + Object log = null; + try { + if (Boolean.getBoolean("io.perfmark.PerfMark.debug")) { + Logger localLogger = Logger.getLogger(PerfMarkImpl.class.getName()); + log = localLogger; + for (Throwable problem : problems) { + if (problem == null) { + continue; + } + localLogger.log(Level.FINE, "Error loading Generator", problem); + } + localLogger.log(Level.FINE, "Using {0}", new Object[] {generator.getClass().getName()}); + logEnabledChange(startEnabled, startEnabledSuccess); + } + } catch (Throwable t) { + // ignore + } + logger = log; + } + + public PerfMarkImpl(Tag key) { + super(key); + } + + @Override + protected synchronized void setEnabled(boolean value) { + boolean changed = setEnabledQuiet(value, System.nanoTime()); + logEnabledChange(value, changed); + } + + @Override + protected synchronized boolean setEnabled(boolean value, boolean overload) { + boolean changed = setEnabledQuiet(value, System.nanoTime()); + logEnabledChange(value, changed); + return changed; + } + + private static synchronized void logEnabledChange(boolean value, boolean success) { + if (success && logger != null) { + Logger localLogger = (Logger) logger; + if (localLogger.isLoggable(Level.FINE)) { + localLogger.fine((value ? "Enabling" : "Disabling") + " PerfMark recorder"); + } + } + } + + /** Returns true if successfully changed. */ + private static synchronized boolean setEnabledQuiet(boolean value, long now) { + if (isEnabled(actualGeneration) == value) { + return false; + } + if (actualGeneration == Generator.FAILURE) { + return false; + } + long nanoDiff = now - Generator.INIT_NANO_TIME; + generator.setGeneration(actualGeneration = nextGeneration(actualGeneration, nanoDiff)); + return true; + } + + // VisibleForTesting + static long nextGeneration(final long currentGeneration, final long nanosSinceInit) { + assert currentGeneration != Generator.FAILURE; + long currentMibros = mibrosFromGeneration(currentGeneration); + long mibrosSinceInit = Math.min(mibrosFromNanos(nanosSinceInit), MAX_MIBROS); // 54bits + boolean nextEnabled = !isEnabled(currentGeneration); + long nextMibros; + if (mibrosSinceInit > currentMibros) { + nextMibros = mibrosSinceInit; + } else { + nextMibros = currentMibros + (nextEnabled ? 1 : 0); + } + if (nextMibros > MAX_MIBROS || nextMibros < 0) { + return Generator.FAILURE; + } + long enabledMask = nextEnabled ? INCREMENT : 0; + long mibroMask = (nextMibros << (Generator.GEN_OFFSET + ENABLED_BIT_SPACE)); + assert (enabledMask & mibroMask) == 0; + return mibroMask | enabledMask; + } + + private static long mibrosFromGeneration(long currentGeneration) { + if (currentGeneration == Generator.FAILURE) { + throw new IllegalArgumentException(); + } + return currentGeneration >>> (Generator.GEN_OFFSET + ENABLED_BIT_SPACE); + } + + private static long mibrosFromNanos(long nanos) { + long remainder = ((1L<<(64 - GEN_TIMESTAMP_SPACE)) - 1) & nanos; + return (nanos >>> (64 - GEN_TIMESTAMP_SPACE)) + + (remainder >= (1L<<(64 - GEN_TIMESTAMP_SPACE - 1)) ? 1 : 0); + } + + @Override + protected void startTask(String taskName, Tag tag) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.startAnyway(gen, taskName, unpackTagName(tag), unpackTagId(tag)); + } + + @Override + protected void startTask(String taskName) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.startAnyway(gen, taskName); + } + + @Override + protected void startTask(String taskName, String subTaskName) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.startAnyway(gen, taskName, subTaskName); + } + + @Override + protected <T> void startTask(T taskNameObject, StringFunction<? super T> stringFunction) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + String taskName = deriveTaskValue(taskNameObject, stringFunction); + Storage.startAnyway(gen, taskName); + } + + @Override + protected void stopTask() { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.stopAnyway(gen); + } + + @Override + protected void stopTask(String taskName, Tag tag) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.stopAnyway(gen, taskName, unpackTagName(tag), unpackTagId(tag)); + } + + @Override + protected void stopTask(String taskName) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.stopAnyway(gen, taskName); + } + + @Override + protected void stopTask(String taskName, String subTaskName) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.stopAnyway(gen, taskName, subTaskName); + } + + @Override + protected void event(String eventName, Tag tag) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.eventAnyway(gen, eventName, unpackTagName(tag), unpackTagId(tag)); + } + + @Override + protected void event(String eventName) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.eventAnyway(gen, eventName); + } + + @Override + protected void event(String eventName, String subEventName) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.eventAnyway(gen, eventName, subEventName); + } + + @Override + protected void attachTag(Tag tag) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.attachTagAnyway(gen, unpackTagName(tag), unpackTagId(tag)); + } + + @Override + protected void attachTag(String tagName, String tagValue) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.attachKeyedTagAnyway(gen, tagName, tagValue); + } + + @Override + protected <T> void attachTag( + String tagName, T tagObject, StringFunction<? super T> stringFunction) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + String tagValue = deriveTagValue(tagName, tagObject, stringFunction); + Storage.attachKeyedTagAnyway(gen, tagName, tagValue); + } + + static <T> String deriveTagValue( + String tagName, T tagObject, StringFunction<? super T> stringFunction) { + try { + return stringFunction.apply(tagObject); + } catch (Throwable t) { + handleTagValueFailure(tagName, tagObject, stringFunction, t); + return "PerfMarkTagError:" + t.getClass().getName(); + } + } + + static <T> String deriveTaskValue(T taskNameObject, StringFunction<? super T> stringFunction) { + try { + return stringFunction.apply(taskNameObject); + } catch (Throwable t) { + handleTaskNameFailure(taskNameObject, stringFunction, t); + return "PerfMarkTaskError:" + t.getClass().getName(); + } + } + + static <T> void handleTagValueFailure( + String tagName, T tagObject, StringFunction<? super T> stringFunction, Throwable cause) { + if (logger == null) { + return; + } + Logger localLogger = (Logger) logger; + try { + if (localLogger.isLoggable(Level.FINE)) { + LogRecord lr = + new LogRecord( + Level.FINE, + "PerfMark.attachTag failed: tagName={0}, tagObject={1}, stringFunction={2}"); + lr.setParameters(new Object[] {tagName, tagObject, stringFunction}); + lr.setThrown(cause); + localLogger.log(lr); + } + } catch (Throwable t) { + // Need to be careful here. It's possible that the Exception thrown may itself throw + // while trying to convert it to a String. Instead, only pass the class name, which is + // safer than passing the whole object. + localLogger.log( + Level.FINE, + "PerfMark.attachTag failed for {0}: {1}", + new Object[] {tagName, t.getClass()}); + } + } + + static <T> void handleTaskNameFailure( + T taskNameObject, StringFunction<? super T> stringFunction, Throwable cause) { + if (logger == null) { + return; + } + Logger localLogger = (Logger) logger; + try { + if (localLogger.isLoggable(Level.FINE)) { + LogRecord lr = + new LogRecord( + Level.FINE, "PerfMark.startTask failed: taskObject={0}, stringFunction={1}"); + lr.setParameters(new Object[] {taskNameObject, stringFunction}); + lr.setThrown(cause); + localLogger.log(lr); + } + } catch (Throwable t) { + // Need to be careful here. It's possible that the Exception thrown may itself throw + // while trying to convert it to a String. Instead, only pass the class name, which is + // safer than passing the whole object. + localLogger.log(Level.FINE, "PerfMark.startTask failed for {0}", new Object[] {t.getClass()}); + } + } + + @Override + protected void attachTag(String tagName, long tagValue) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.attachKeyedTagAnyway(gen, tagName, tagValue); + } + + @Override + protected void attachTag(String tagName, long tagValue0, long tagValue1) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.attachKeyedTagAnyway(gen, tagName, tagValue0, tagValue1); + } + + @Override + protected Tag createTag(@Nullable String tagName, long tagId) { + if (!isEnabled(getGen())) { + return NO_TAG; + } + return packTag(tagName, tagId); + } + + @Override + protected Link linkOut() { + final long gen = getGen(); + if (!isEnabled(gen)) { + return NO_LINK; + } + long linkId = linkIdAlloc.getAndIncrement(); + Storage.linkAnyway(gen, linkId); + return packLink(linkId); + } + + @Override + protected void linkIn(Link link) { + final long gen = getGen(); + if (!isEnabled(gen)) { + return; + } + Storage.linkAnyway(gen, -unpackLinkId(link)); + } + + private static long getGen() { + return generator.getGeneration(); + } + + private static boolean isEnabled(long gen) { + return ((gen >>> Generator.GEN_OFFSET) & 0x1L) != 0L; + } + } + + private SecretPerfMarkImpl() {} +} diff --git a/impl/src/main/java/io/perfmark/impl/Storage.java b/impl/src/main/java/io/perfmark/impl/Storage.java new file mode 100644 index 0000000..9cbda73 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/Storage.java @@ -0,0 +1,468 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; + +/** + * Storage is responsible for storing and returning recorded marks. This is a low level class and + * not intended for use by users. Instead, the {@code TraceEventWriter} and {@code TraceEventViewer} + * classes provide easier to use APIs for accessing PerfMark data. + * + * <p>This code is <strong>NOT</strong> API stable, and may be removed in the future, or changed + * without notice. + */ +public final class Storage { + static final AtomicLong markHolderIdAllocator = new AtomicLong(1); + // The order of initialization here matters. If a logger invokes PerfMark, it will be re-entrant + // and need to use these static variables. + static final ConcurrentMap<Reference<Thread>, MarkHolderHandle> allMarkHolders = + new ConcurrentHashMap<Reference<Thread>, MarkHolderHandle>(); + private static final LocalMarkHolder localMarkHolder = new MostlyThreadLocalMarkHolder(); + private static final MarkHolderProvider markHolderProvider; + private static final ReferenceQueue<Thread> threadReferenceQueue = new ReferenceQueue<Thread>(); + private static volatile long lastGlobalIndexClear = Generator.INIT_NANO_TIME - 1; + + static { + MarkHolderProvider provider = null; + Throwable[] problems = new Throwable[3]; + try { + String markHolderOverride = System.getProperty("io.perfmark.PerfMark.markHolderProvider"); + if (markHolderOverride != null && !markHolderOverride.isEmpty()) { + Class<?> clz = Class.forName(markHolderOverride); + provider = clz.asSubclass(MarkHolderProvider.class).getConstructor().newInstance(); + } + } catch (Throwable t) { + problems[0] = t; + } + if (provider == null) { + try { + Class<?> clz = + Class.forName( + "io.perfmark.java9.SecretVarHandleMarkHolderProvider$VarHandleMarkHolderProvider"); + provider = clz.asSubclass(MarkHolderProvider.class).getConstructor().newInstance(); + } catch (Throwable t) { + problems[1] = t; + } + } + if (provider == null) { + try { + Class<?> clz = + Class.forName( + "io.perfmark.java6.SecretSynchronizedMarkHolderProvider$SynchronizedMarkHolderProvider"); + provider = clz.asSubclass(MarkHolderProvider.class).getConstructor().newInstance(); + } catch (Throwable t) { + problems[2] = t; + } + } + if (provider == null) { + markHolderProvider = new NoopMarkHolderProvider(); + } else { + markHolderProvider = provider; + } + try { + if (Boolean.getBoolean("io.perfmark.PerfMark.debug")) { + // See the comment in io.perfmark.PerfMark for why this is invoked reflectively. + Class<?> logClass = Class.forName("java.util.logging.Logger"); + Object logger = logClass.getMethod("getLogger", String.class).invoke(null, Storage.class.getName()); + Class<?> levelClass = Class.forName("java.util.logging.Level"); + Object level = levelClass.getField("FINE").get(null); + Method logProblemMethod = logClass.getMethod("log", levelClass, String.class, Throwable.class); + + for (Throwable problem : problems) { + if (problem == null) { + continue; + } + logProblemMethod.invoke(logger, level, "Error loading MarkHolderProvider", problem); + } + Method logSuccessMethod = logClass.getMethod("log", levelClass, String.class, Object[].class); + logSuccessMethod.invoke(logger, level, "Using {0}", new Object[] {markHolderProvider.getClass().getName()}); + } + } catch (Throwable t) { + // ignore + } + } + + public static long getInitNanoTime() { + return Generator.INIT_NANO_TIME; + } + + /** + * Returns a list of {@link MarkList}s across all reachable threads. + * + * @return all reachable MarkLists. + */ + public static List<MarkList> read() { + long lastReset = lastGlobalIndexClear; + drainThreadQueue(); + List<MarkList> markLists = new ArrayList<MarkList>(allMarkHolders.size()); + for (Iterator<MarkHolderHandle> it = allMarkHolders.values().iterator(); it.hasNext();) { + MarkHolderHandle handle = it.next(); + Thread writer = handle.threadRef().get(); + if (writer == null) { + handle.softenMarkHolderReference(); + } + MarkHolder markHolder = handle.markHolder(); + if (markHolder == null) { + it.remove(); + handle.clearSoftReference(); + continue; + } + String threadName = handle.getAndUpdateThreadName(); + long threadId = handle.getAndUpdateThreadId(); + boolean concurrentWrites = !(Thread.currentThread() == writer || writer == null); + markLists.add( + MarkList.newBuilder() + .setMarks(markHolder.read(concurrentWrites)) + .setThreadName(threadName) + .setThreadId(threadId) + .setMarkListId(handle.markHolderId) + .build()); + } + return Collections.unmodifiableList(markLists); + } + + static void startAnyway(long gen, String taskName, @Nullable String tagName, long tagId) { + MarkHolder mh = localMarkHolder.acquire(); + mh.start(gen, taskName, tagName, tagId, System.nanoTime()); + localMarkHolder.release(mh); + } + + static void startAnyway(long gen, String taskName) { + MarkHolder mh = localMarkHolder.acquire(); + mh.start(gen, taskName, System.nanoTime()); + localMarkHolder.release(mh); + } + + static void startAnyway(long gen, String taskName, String subTaskName) { + MarkHolder mh = localMarkHolder.acquire(); + mh.start(gen, taskName, subTaskName, System.nanoTime()); + localMarkHolder.release(mh); + } + + static void stopAnyway(long gen) { + long nanoTime = System.nanoTime(); + MarkHolder mh = localMarkHolder.acquire(); + mh.stop(gen, nanoTime); + localMarkHolder.release(mh); + } + + static void stopAnyway(long gen, String taskName, @Nullable String tagName, long tagId) { + long nanoTime = System.nanoTime(); + MarkHolder mh = localMarkHolder.acquire(); + mh.stop(gen, taskName, tagName, tagId, nanoTime); + localMarkHolder.release(mh); + } + + static void stopAnyway(long gen, String taskName) { + long nanoTime = System.nanoTime(); + MarkHolder mh = localMarkHolder.acquire(); + mh.stop(gen, taskName, nanoTime); + localMarkHolder.release(mh); + } + + static void stopAnyway(long gen, String taskName, String subTaskName) { + long nanoTime = System.nanoTime(); + MarkHolder mh = localMarkHolder.acquire(); + mh.stop(gen, taskName, subTaskName, nanoTime); + localMarkHolder.release(mh); + } + + static void eventAnyway(long gen, String eventName, @Nullable String tagName, long tagId) { + long nanoTime = System.nanoTime(); + MarkHolder mh = localMarkHolder.acquire(); + mh.event(gen, eventName, tagName, tagId, nanoTime); + localMarkHolder.release(mh); + } + + static void eventAnyway(long gen, String eventName) { + long nanoTime = System.nanoTime(); + MarkHolder mh = localMarkHolder.acquire(); + mh.event(gen, eventName, nanoTime); + localMarkHolder.release(mh); + } + + static void eventAnyway(long gen, String eventName, String subEventName) { + long nanoTime = System.nanoTime(); + MarkHolder mh = localMarkHolder.acquire(); + mh.event(gen, eventName, subEventName, nanoTime); + localMarkHolder.release(mh); + } + + static void linkAnyway(long gen, long linkId) { + MarkHolder mh = localMarkHolder.acquire(); + mh.link(gen, linkId); + localMarkHolder.release(mh); + } + + static void attachTagAnyway(long gen, @Nullable String tagName, long tagId) { + MarkHolder mh = localMarkHolder.acquire(); + mh.attachTag(gen, tagName, tagId); + localMarkHolder.release(mh); + } + + static void attachKeyedTagAnyway(long gen, @Nullable String tagName, String tagValue) { + MarkHolder mh = localMarkHolder.acquire(); + mh.attachKeyedTag(gen, tagName, tagValue); + localMarkHolder.release(mh); + } + + static void attachKeyedTagAnyway(long gen, @Nullable String tagName, long tagValue) { + MarkHolder mh = localMarkHolder.acquire(); + mh.attachKeyedTag(gen, tagName, tagValue); + localMarkHolder.release(mh); + } + + static void attachKeyedTagAnyway( + long gen, @Nullable String tagName, long tagValue0, long tagValue1) { + MarkHolder mh = localMarkHolder.acquire(); + mh.attachKeyedTag(gen, tagName, tagValue0, tagValue1); + localMarkHolder.release(mh); + } + + /** + * Removes all data for the calling Thread. Other threads may Still have stored data. + */ + public static void clearLocalStorage() { + for (Iterator<MarkHolderHandle> it = allMarkHolders.values().iterator(); it.hasNext();) { + MarkHolderHandle handle = it.next(); + if (handle.threadRef.get() == Thread.currentThread()) { + it.remove(); + handle.threadRef.clearInternal(); + handle.softenMarkHolderReference(); + handle.clearSoftReference(); + } + } + localMarkHolder.clear(); + } + + /** + * Removes the global Read index on all storage, but leaves local storage in place. Because writer threads may still + * be writing to the same buffer (which they have a strong ref to), this function only removed data that is truly + * unwritable anymore. In addition, it captures a timestamp to which marks to include when reading. Thus, the data + * isn't fully removed. To fully remove all data, each tracing thread must call {@link #clearLocalStorage}. + */ + public static void clearGlobalIndex() { + lastGlobalIndexClear = System.nanoTime() - 1; + for (Iterator<MarkHolderHandle> it = allMarkHolders.values().iterator(); it.hasNext();) { + MarkHolderHandle handle = it.next(); + handle.softenMarkHolderReference(); + Thread writer = handle.threadRef().get(); + if (writer == null) { + it.remove(); + handle.clearSoftReference(); + } + } + } + + private static void drainThreadQueue() { + while (true) { + Reference<?> ref = threadReferenceQueue.poll(); + if (ref == null) { + return; + } + MarkHolderHandle handle = allMarkHolders.get(ref); + if (handle != null) { + handle.softenMarkHolderReference(); + if (handle.markHolder() == null) { + allMarkHolders.remove(ref); + handle.clearSoftReference(); + } + } + } + } + + @Nullable + public static MarkList readForTest() { + List<MarkList> lists = read(); + for (MarkList list : lists) { + // This is slightly wrong as the thread ID could be reused. + if (list.getThreadId() == Thread.currentThread().getId()) { + return list; + } + } + return null; + } + + public static final class MarkHolderHandle { + private static final SoftReference<MarkHolder> EMPTY = new SoftReference<MarkHolder>(null); + + private final UnmodifiableWeakReference<Thread> threadRef; + private final AtomicReference<MarkHolder> markHolderRef; + private volatile SoftReference<MarkHolder> softMarkHolderRef; + + private volatile String threadName; + private volatile long threadId; + private final long markHolderId; + + MarkHolderHandle(Thread thread, MarkHolder markHolder, long markHolderId) { + this.threadRef = new UnmodifiableWeakReference<Thread>(thread, threadReferenceQueue); + this.markHolderRef = new AtomicReference<MarkHolder>(markHolder); + this.threadName = thread.getName(); + this.threadId = thread.getId(); + this.markHolderId = markHolderId; + } + + /** + * Returns the MarkHolder. May return {@code null} if the Thread is gone. If {@code null} is returned, + * then {@code getThreadRef().get() == null}. If a non-{@code null} value is returned, the thread may be dead or + * alive. Additionally, since the {@link #threadRef} may be externally cleared, it is not certain that the Thread + * is dead. + */ + public MarkHolder markHolder() { + MarkHolder markHolder = markHolderRef.get(); + if (markHolder == null) { + markHolder = softMarkHolderRef.get(); + assert markHolder != null || threadRef.get() == null; + } + return markHolder; + } + + /** + * Returns a weak reference to the Thread that created the MarkHolder. + */ + public WeakReference<? extends Thread> threadRef() { + return threadRef; + } + + void softenMarkHolderReference() { + synchronized (markHolderRef) { + MarkHolder markHolder = markHolderRef.get(); + if (markHolder != null) { + softMarkHolderRef = new SoftReference<MarkHolder>(markHolder); + markHolderRef.set(null); + } + } + } + + void clearSoftReference() { + Thread thread = threadRef.get(); + if (thread != null) { + throw new IllegalStateException("Thread still alive " + thread); + } + synchronized (markHolderRef) { + MarkHolder markHolder = markHolderRef.get(); + if (markHolder != null) { + throw new IllegalStateException("Handle not yet softened"); + } + softMarkHolderRef.clear(); + softMarkHolderRef = EMPTY; + threadName = null; + threadId = -255; + } + } + + String getAndUpdateThreadName() { + Thread t = threadRef.get(); + String name; + if (t != null) { + threadName = (name = t.getName()); + } else { + name = threadName; + } + return name; + } + + /** + * Some threads change their id over time, so we need to sync it if available. + */ + long getAndUpdateThreadId() { + Thread t = threadRef.get(); + long id; + if (t != null) { + threadId = (id = t.getId()); + } else { + id = threadId; + } + return id; + } + } + + /** + * This class is needed to work around a race condition where a newly created MarkHolder could be GC'd before + * the caller of {@link #allocateMarkHolder} can consume the results. The provided MarkHolder is strongly reachable. + */ + public static final class MarkHolderAndHandle { + private final MarkHolder markHolder; + private final MarkHolderHandle handle; + + MarkHolderAndHandle(MarkHolder markHolder, MarkHolderHandle handle) { + this.markHolder = markHolder; + this.handle = handle; + + MarkHolder tmp = handle.markHolder(); + if (markHolder != tmp && tmp != null) { + throw new IllegalArgumentException("Holder Handle mismatch"); + } + } + + public MarkHolder markHolder() { + return markHolder; + } + + public MarkHolderHandle handle() { + return handle; + } + } + + public static MarkHolderAndHandle allocateMarkHolder() { + drainThreadQueue(); + long markHolderId = markHolderIdAllocator.getAndIncrement(); + MarkHolder holder = markHolderProvider.create(markHolderId); + MarkHolderHandle handle = new MarkHolderHandle(Thread.currentThread(), holder, markHolderId); + allMarkHolders.put(handle.threadRef, handle); + return new MarkHolderAndHandle(holder, handle); + } + + private static final class UnmodifiableWeakReference<T> extends WeakReference<T> { + + UnmodifiableWeakReference(T referent, ReferenceQueue<T> q) { + super(referent, q); + } + + @Override + @Deprecated + @SuppressWarnings("InlineMeSuggester") + public void clear() {} + + @Override + @Deprecated + @SuppressWarnings("InlineMeSuggester") + public boolean enqueue() { + return false; + } + + void clearInternal() { + super.clear(); + } + } + + private Storage() {} +} diff --git a/impl/src/main/java/io/perfmark/impl/package-info.java b/impl/src/main/java/io/perfmark/impl/package-info.java new file mode 100644 index 0000000..a854569 --- /dev/null +++ b/impl/src/main/java/io/perfmark/impl/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +@javax.annotation.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +package io.perfmark.impl; diff --git a/impl/src/test/java/io/perfmark/impl/PerfMarkImplTest.java b/impl/src/test/java/io/perfmark/impl/PerfMarkImplTest.java new file mode 100644 index 0000000..9a9259d --- /dev/null +++ b/impl/src/test/java/io/perfmark/impl/PerfMarkImplTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 Google LLC + * + * 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 io.perfmark.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class PerfMarkImplTest { + + @Test + public void nextGeneration_enable() { + var gen = SecretPerfMarkImpl.PerfMarkImpl.nextGeneration(0, 3*1024); + assertEquals((3L<<10) + (1<<8), gen); + } + + @Test + public void nextGeneration_disable() { + var gen = SecretPerfMarkImpl.PerfMarkImpl.nextGeneration(1 << Generator.GEN_OFFSET, 3*1024); + assertEquals((3L<<10), gen); + } + + @Test + public void nextGeneration_newStampEnabled() { + var gen = SecretPerfMarkImpl.PerfMarkImpl.nextGeneration( + (3<< (Generator.GEN_OFFSET + 2)) + + (1 << Generator.GEN_OFFSET), + 3*1024); + assertEquals((3L<<10), gen); + } + + @Test + public void nextGeneration_newStampDisabled() { + var gen = SecretPerfMarkImpl.PerfMarkImpl.nextGeneration( + (3<< (Generator.GEN_OFFSET + 2)), + 3*1024); + assertEquals((4L<<10) + (1<<Generator.GEN_OFFSET), gen); + } + + @Test + public void nextGeneration_maxNanos() { + var gen = SecretPerfMarkImpl.PerfMarkImpl.nextGeneration(1 << Generator.GEN_OFFSET, -1); + assertNotEquals(Generator.FAILURE, gen); + } + + @Test + public void nextGeneration_noOverflowOnDisable() { + var gen = SecretPerfMarkImpl.PerfMarkImpl.nextGeneration(0xFFFF_FFFF_FFFF_FD00L, -1); + assertNotEquals(Generator.FAILURE, gen); + } + + @Test + public void nextGeneration_overflowOnEnable() { + var gen = SecretPerfMarkImpl.PerfMarkImpl.nextGeneration(0xFFFF_FFFF_FFFF_FC00L, -1); + assertEquals(Generator.FAILURE, gen); + } +} diff --git a/impl/src/test/java/io/perfmark/impl/StorageTest.java b/impl/src/test/java/io/perfmark/impl/StorageTest.java new file mode 100644 index 0000000..ebe9e0f --- /dev/null +++ b/impl/src/test/java/io/perfmark/impl/StorageTest.java @@ -0,0 +1,189 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.google.common.truth.Truth; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class StorageTest { + + @Test + public void threadsCleanedUp() throws Exception { + Storage.clearLocalStorage(); + final CountDownLatch latch = new CountDownLatch(1); + new Thread( + new Runnable() { + @Override + public void run() { + Storage.clearLocalStorage(); + Storage.linkAnyway(4096, 1234); + latch.countDown(); + } + }) + .start(); + + for (int i = 10; i < 5000; i += i / 2) { + latch.await(i, TimeUnit.MILLISECONDS); + System.gc(); + System.runFinalization(); + } + + assertEquals(0, latch.getCount()); + List<MarkList> firstRead = Storage.read(); + assertEquals(1, firstRead.size()); + // simulate an OOM + Storage.clearGlobalIndex(); + List<MarkList> secondRead = Storage.read(); + assertEquals(0, secondRead.size()); + } + + @Test + public void customMarkHolderImpl() throws Exception { + Class<?> clz = runWithProperty( + System.getProperties(), + "io.perfmark.PerfMark.markHolderProvider", + TestMarkHolderProvider.class.getName(), + () -> Class.forName(Storage.class.getName(), true, new TestClassLoader(getClass().getClassLoader()))); + + Field field = clz.getDeclaredField("markHolderProvider"); + field.setAccessible(true); + Object value = field.get(null); + assertNotNull(value); + // Can't do instanceof, since class loaders are different. + assertEquals(TestMarkHolderProvider.class.getName(), value.getClass().getName()); + } + @Test + public void logEnabled() throws Exception { + ClassLoader loader = new TestClassLoader(getClass().getClassLoader()); + List<LogRecord> logs = new ArrayList<>(); + Filter filter = record -> { + logs.add(record); + return true; + }; + Logger logger = Logger.getLogger(Storage.class.getName()); + Level oldLevel = logger.getLevel(); + Filter oldFilter = logger.getFilter(); + logger.setLevel(Level.ALL); + logger.setFilter(filter); + try { + runWithProperty(System.getProperties(), "io.perfmark.PerfMark.debug", "true", () -> { + // Force Initialization. + Class.forName(Storage.class.getName(), true, loader); + return null; + }); + } finally{ + logger.setFilter(oldFilter); + logger.setLevel(oldLevel); + } + + // This depends on the the classpath being set up correctly. + Truth.assertThat(logs).hasSize(3); + Truth.assertThat(logs.get(0).getMessage()).contains("Error loading MarkHolderProvider"); + Truth.assertThat(logs.get(0).getThrown()).hasMessageThat() + .contains("io.perfmark.java9.SecretVarHandleMarkHolderProvider$VarHandleMarkHolderProvider"); + Truth.assertThat(logs.get(1).getMessage()).contains("Error loading MarkHolderProvider"); + Truth.assertThat(logs.get(1).getThrown()).hasMessageThat() + .contains("io.perfmark.java6.SecretSynchronizedMarkHolderProvider$SynchronizedMarkHolderProvider"); + Truth.assertThat(new SimpleFormatter().format(logs.get(2))) + .contains("Using io.perfmark.impl.NoopMarkHolderProvider"); + } + + public static final class TestMarkHolderProvider extends MarkHolderProvider { + @Override + public MarkHolder create(long markHolderId) { + throw new AssertionError(); + } + } + + private static class TestClassLoader extends ClassLoader { + + private final List<String> classesToDrop; + + TestClassLoader(ClassLoader parent, String ... classesToDrop) { + super(parent); + this.classesToDrop = Arrays.asList(classesToDrop); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (classesToDrop.contains(name)) { + throw new ClassNotFoundException(); + } + if (!name.startsWith("io.perfmark.")) { + return super.loadClass(name, resolve); + } + try (InputStream is = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) { + if (is == null) { + throw new ClassNotFoundException(name); + } + byte[] data = is.readAllBytes(); + Class<?> clz = defineClass(name, data, 0, data.length); + if (resolve) { + resolveClass(clz); + } + return clz; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @CanIgnoreReturnValue + private static <T> T runWithProperty(Properties properties, String name, String value, Callable<T> runnable) + throws Exception { + if (properties.containsKey(name)) { + String oldProp; + oldProp = properties.getProperty(name); + try { + System.setProperty(name, value); + return runnable.call(); + } finally{ + properties.setProperty(name, oldProp); + } + } else { + try { + System.setProperty(name, value); + return runnable.call(); + } finally{ + properties.remove(name); + } + } + } + +} diff --git a/java15/build.gradle.kts b/java15/build.gradle.kts new file mode 100644 index 0000000..b6d7e9f --- /dev/null +++ b/java15/build.gradle.kts @@ -0,0 +1,79 @@ +import net.ltgt.gradle.errorprone.errorprone + +plugins { + id("io.github.reyerizo.gradle.jcstress") +} + +buildscript { + extra.apply{ + set("moduleName", "io.perfmark.javafifteen") + } +} + +val jdkVersion = JavaVersion.VERSION_15 + +description = "PerfMark Java15 API" + +sourceSets { + create("jmh") +} + +val jmhImplementation by configurations.getting { + extendsFrom(configurations.implementation.get()) +} + +val jmhAnnotationProcessor by configurations.getting { + extendsFrom(configurations.annotationProcessor.get()) +} + +dependencies { + implementation(project(":perfmark-impl")) + compileOnly(libs.jsr305) + + testImplementation(project(":perfmark-api")) + testImplementation(project(":perfmark-testing")) + jcstressImplementation(project(":perfmark-impl")) + + jmhImplementation(project(":perfmark-api")) + jmhImplementation(project(":perfmark-impl")) + jmhImplementation(project(":perfmark-java15")) + jmhImplementation(project(":perfmark-testing")) + + jmhImplementation(libs.junit) + jmhImplementation(libs.jmhcore) + jmhAnnotationProcessor(libs.jmhanno) +} + +tasks.named<JavaCompile>("compileJava") { + sourceCompatibility = jdkVersion.toString() + targetCompatibility = jdkVersion.toString() +} + +tasks.named<Javadoc>("javadoc") { + exclude("io/perfmark/java15/**") +} + +tasks.register<Test>("jmh") { + description = "Runs integration tests." + group = "stress" + + testClassesDirs = sourceSets["jmh"].output.classesDirs + classpath = sourceSets["jmh"].runtimeClasspath +} + +// ./gradlew --no-daemon clean :perfmark-java9:jcstress +jcstress { + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.5" + // mode "tough" + deoptRatio = "2" +} + + +tasks.named<JavaCompile>("compileJmhJava") { + options.errorprone.excludedPaths.set(".*/build/generated/sources/annotationProcessor/.*") +} + + +tasks.named<JavaCompile>("compileJcstressJava") { + options.errorprone.excludedPaths.set(".*/build/generated/sources/annotationProcessor/.*") +} diff --git a/java15/src/jmh/java/io/perfmark/java15/HiddenClassVarHandleMarkerBenchmarkTest.java b/java15/src/jmh/java/io/perfmark/java15/HiddenClassVarHandleMarkerBenchmarkTest.java new file mode 100644 index 0000000..2836f0b --- /dev/null +++ b/java15/src/jmh/java/io/perfmark/java15/HiddenClassVarHandleMarkerBenchmarkTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java15; + +import io.perfmark.impl.MarkHolder; +import io.perfmark.testing.MarkHolderBenchmark; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; + +@RunWith(JUnit4.class) +public class HiddenClassVarHandleMarkerBenchmarkTest { + @Test + public void markHolderBenchmark() throws Exception { + Options options = new OptionsBuilder() + .include(SecretHiddenClassMarkHolderBenchmark.class.getCanonicalName()) + .addProfiler("perfasm") + .measurementIterations(10) + .warmupIterations(10) + .forks(1) + .warmupTime(TimeValue.seconds(1)) + .measurementTime(TimeValue.seconds(1)) + .shouldFailOnError(true) + // This is necessary to run in the IDE, otherwise it would inherit the VM args. + .jvmArgs( + "-da", + "-XX:+UnlockExperimentalVMOptions", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+UseEpsilonGC", + "-XX:+LogCompilation", + "-XX:LogFile=/dev/null", + "-XX:+PrintAssembly", + "-XX:+PrintInterpreter", + "-XX:+PrintNMethods", + "-XX:+PrintNativeNMethods", + "-XX:+PrintSignatureHandlers", + "-XX:+PrintAdapterHandlers", + "-XX:+PrintStubCode", + "-XX:+PrintCompilation", + "-XX:+PrintInlining", + "-XX:PrintAssemblyOptions=syntax", + "-XX:PrintAssemblyOptions=intel") + .build(); + + new Runner(options).run(); + } + + @State(Scope.Thread) + public static class SecretHiddenClassMarkHolderBenchmark extends MarkHolderBenchmark { + @Override + public MarkHolder getMarkHolder() { + return new SecretHiddenClassMarkHolderProvider.HiddenClassMarkHolderProvider().create(1234, 16384); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public MarkHolder allocationBenchmark() { + return new SecretHiddenClassMarkHolderProvider.HiddenClassMarkHolderProvider().create(1234, 4); + } + } +} diff --git a/java15/src/main/java/io/perfmark/java15/HiddenClassVarHandleMarkHolder.java b/java15/src/main/java/io/perfmark/java15/HiddenClassVarHandleMarkHolder.java new file mode 100644 index 0000000..c15396d --- /dev/null +++ b/java15/src/main/java/io/perfmark/java15/HiddenClassVarHandleMarkHolder.java @@ -0,0 +1,393 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java15; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkHolder; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Deque; +import java.util.List; + +/** HiddenClassVarHandleMarkHolder is a MarkHolder optimized for wait free writes and few reads. */ +final class HiddenClassVarHandleMarkHolder extends MarkHolder { + private static final long GEN_MASK = (1 << Generator.GEN_OFFSET) - 1; + private static final long START_OP = 1; // Mark.Operation.TASK_START.ordinal(); + private static final long START_S_OP = 2; + private static final long START_T_OP = 3; // Mark.Operation.TASK_START_T.ordinal(); + private static final long STOP_OP = 4; // Mark.Operation.TASK_END.ordinal(); + private static final long STOP_V_OP = 5; + private static final long STOP_T_OP = 6; // Mark.Operation.TASK_END_T.ordinal(); + private static final long STOP_S_OP = 7; + private static final long EVENT_OP = 8; // Mark.Operation.EVENT.ordinal(); + private static final long EVENT_T_OP = 9; // Mark.Operation.EVENT_T.ordinal(); + private static final long EVENT_S_OP = 10; + private static final long LINK_OP = 11; // Mark.Operation.LINK.ordinal(); + private static final long ATTACH_T_OP = 12; // Mark.Operation.ATTACH_TAG.ordinal(); + private static final long ATTACH_SS_OP = 13; + private static final long ATTACH_SN_OP = 14; + private static final long ATTACH_SNN_OP = 15; + + private static final VarHandle IDX; + private static final VarHandle STRINGS; + private static final VarHandle LONGS; + + /** This is a magic number, read the top level doc for explanation. */ + static final int MAX_EVENTS = 0x7e3779b9; + static final long MAX_EVENTS_MASK = MAX_EVENTS - 1; + + // where to write to next + @SuppressWarnings("unused") // Used Reflectively + private static volatile long idx; + + private static final String[] taskNames; + private static final String[] tagNames; + private static final long[] tagIds; + private static final long[] nanoTimes; + private static final long[] genOps; + + static { + try { + IDX = MethodHandles.lookup().findStaticVarHandle(HiddenClassVarHandleMarkHolder.class, "idx", long.class); + STRINGS = MethodHandles.arrayElementVarHandle(String[].class); + LONGS = MethodHandles.arrayElementVarHandle(long[].class); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + try { + int maxEvents = (int) HiddenClassVarHandleMarkHolder.class.getDeclaredField("MAX_EVENTS").get(null); + if (((maxEvents - 1) & maxEvents) != 0) { + throw new IllegalArgumentException(maxEvents + " is not a power of two"); + } + if (maxEvents <= 0) { + throw new IllegalArgumentException(maxEvents + " is not positive"); + } + long maxEventsMask = (long) HiddenClassVarHandleMarkHolder.class.getDeclaredField("MAX_EVENTS_MASK").get(null); + if (maxEvents - 1 != maxEventsMask) { + throw new IllegalArgumentException(maxEvents + " doesn't match mask " + maxEventsMask); + } + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + + taskNames = new String[MAX_EVENTS]; + tagNames = new String[MAX_EVENTS]; + tagIds = new long[MAX_EVENTS]; + nanoTimes = new long[MAX_EVENTS]; + genOps = new long[MAX_EVENTS]; + } + + HiddenClassVarHandleMarkHolder() {} + + @Override + public void start(long gen, String taskName, String tagName, long tagId, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + START_T_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void start(long gen, String taskName, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, taskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + START_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void start(long gen, String taskName, String subTaskName, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, subTaskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + START_S_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void link(long gen, long linkId) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + LONGS.setOpaque(tagIds, i, linkId); + LONGS.setOpaque(genOps, i, gen + LINK_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_V_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, String taskName, String tagName, long tagId, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_T_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, String taskName, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, taskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, String taskName, String subTaskName, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, subTaskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_S_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void event(long gen, String eventName, String tagName, long tagId, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, eventName); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + EVENT_T_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void event(long gen, String eventName, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, eventName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + EVENT_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void event(long gen, String eventName, String subEventName, long nanoTime) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(taskNames, i, eventName); + STRINGS.setOpaque(tagNames, i, subEventName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + EVENT_S_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachTag(long gen, String tagName, long tagId) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(genOps, i, gen + ATTACH_T_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachKeyedTag(long gen, String name, long value) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(tagNames, i, name); + LONGS.setOpaque(tagIds, i, value); + LONGS.setOpaque(genOps, i, gen + ATTACH_SN_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachKeyedTag(long gen, String name, long value0, long value1) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(tagNames, i, name); + LONGS.setOpaque(tagIds, i, value0); + LONGS.setOpaque(nanoTimes, i, value1); + LONGS.setOpaque(genOps, i, gen + ATTACH_SNN_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachKeyedTag(long gen, String name, String value) { + long localIdx = (long) IDX.get(); + int i = (int) (localIdx & MAX_EVENTS_MASK); + STRINGS.setOpaque(tagNames, i, name); + STRINGS.setOpaque(taskNames, i, value); + LONGS.setOpaque(genOps, i, gen + ATTACH_SS_OP); + IDX.setRelease(localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void resetForTest() { + Arrays.fill(taskNames, null); + Arrays.fill(tagNames, null); + Arrays.fill(tagIds, 0); + Arrays.fill(nanoTimes, 0); + Arrays.fill(genOps, 0); + IDX.setRelease(0L); + VarHandle.storeStoreFence(); + } + + @Override + public List<Mark> read(boolean concurrentWrites) { + final String[] localTaskNames = new String[MAX_EVENTS]; + final String[] localTagNames = new String[MAX_EVENTS]; + final long[] localTagIds = new long[MAX_EVENTS]; + final long[] localNanoTimes = new long[MAX_EVENTS]; + final long[] localGenOps = new long[MAX_EVENTS]; + long startIdx = (long) IDX.getOpaque(); + VarHandle.loadLoadFence(); + int size = (int) Math.min(startIdx, MAX_EVENTS); + for (int i = 0; i < size; i++) { + localTaskNames[i] = (String) STRINGS.getOpaque(taskNames, i); + localTagNames[i] = (String) STRINGS.getOpaque(tagNames, i); + localTagIds[i] = (long) LONGS.getOpaque(tagIds, i); + localNanoTimes[i] = (long) LONGS.getOpaque(nanoTimes, i); + localGenOps[i] = (long) LONGS.getOpaque(genOps, i); + } + VarHandle.loadLoadFence(); + long endIdx = (long) IDX.getOpaque(); + if (endIdx < startIdx) { + throw new AssertionError(); + } + // If we are reading from ourselves (such as in a test), we can assume there isn't an in + // progress write modifying the oldest entry. Additionally, if the writer has not yet + // wrapped around, the last entry cannot have been corrupted. + boolean tailValid = !concurrentWrites || endIdx < MAX_EVENTS - 1; + endIdx += !tailValid ? 1 : 0; + long eventsToDrop = endIdx - startIdx; + final Deque<Mark> marks = new ArrayDeque<>(size); + for (int i = 0; i < size - eventsToDrop; i++) { + int readIdx = (int) ((startIdx - i - 1) & MAX_EVENTS_MASK); + long gen = localGenOps[readIdx] & ~GEN_MASK; + int opVal = (int) (localGenOps[readIdx] & GEN_MASK); + switch (opVal) { + case (int) START_T_OP: + marks.addFirst(Mark.tag(gen, localTagNames[readIdx], localTagIds[readIdx])); + // fallthrough + case (int) START_OP: + marks.addFirst(Mark.taskStart(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + break; + case (int) START_S_OP: + marks.addFirst( + Mark.taskStart( + gen, localNanoTimes[readIdx], localTaskNames[readIdx], localTagNames[readIdx])); + break; + case (int) STOP_V_OP: + marks.addFirst(Mark.taskEnd(gen, localNanoTimes[readIdx])); + break; + case (int) STOP_S_OP: + marks.addFirst( + Mark.taskEnd( + gen, localNanoTimes[readIdx], localTaskNames[readIdx], localTagNames[readIdx])); + break; + case (int) STOP_OP: + marks.addFirst(Mark.taskEnd(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + break; + case (int) STOP_T_OP: + marks.addFirst(Mark.taskEnd(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + marks.addFirst(Mark.tag(gen, localTagNames[readIdx], localTagIds[readIdx])); + break; + case (int) EVENT_OP: + marks.addFirst(Mark.event(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + break; + case (int) EVENT_T_OP: + marks.addFirst( + Mark.event( + gen, + localNanoTimes[readIdx], + localTaskNames[readIdx], + localTagNames[readIdx], + localTagIds[readIdx])); + break; + case (int) EVENT_S_OP: + marks.addFirst( + Mark.event( + gen, localNanoTimes[readIdx], localTaskNames[readIdx], localTagNames[readIdx])); + break; + case (int) LINK_OP: + marks.addFirst(Mark.link(gen, localTagIds[readIdx])); + break; + case (int) ATTACH_T_OP: + marks.addFirst(Mark.tag(gen, localTagNames[readIdx], localTagIds[readIdx])); + break; + case (int) ATTACH_SS_OP: + marks.addFirst(Mark.keyedTag(gen, localTagNames[readIdx], localTaskNames[readIdx])); + break; + case (int) ATTACH_SN_OP: + marks.addFirst(Mark.keyedTag(gen, localTagNames[readIdx], localTagIds[readIdx])); + break; + case (int) ATTACH_SNN_OP: + marks.addFirst( + Mark.keyedTag( + gen, localTagNames[readIdx], localTagIds[readIdx], localNanoTimes[readIdx])); + break; + default: + throw new ConcurrentModificationException("Read of storage was not threadsafe " + opVal); + } + } + + return Collections.unmodifiableList(new ArrayList<>(marks)); + } + + @Override + public int maxMarks() { + return MAX_EVENTS; + } +} diff --git a/java15/src/main/java/io/perfmark/java15/SecretHiddenClassMarkHolderProvider.java b/java15/src/main/java/io/perfmark/java15/SecretHiddenClassMarkHolderProvider.java new file mode 100644 index 0000000..a296f40 --- /dev/null +++ b/java15/src/main/java/io/perfmark/java15/SecretHiddenClassMarkHolderProvider.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java15; + +import io.perfmark.impl.MarkHolder; +import io.perfmark.impl.MarkHolderProvider; +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +public final class SecretHiddenClassMarkHolderProvider { + public static final class HiddenClassMarkHolderProvider extends MarkHolderProvider { + private static final int DEFAULT_SIZE = 32768; + + private final int[] maxEventsOffsets; + private final int[] maxEventsMaskOffsets; + + private final byte[] markHolderClassData; + + public HiddenClassMarkHolderProvider() { + try (InputStream classData = getClass().getResourceAsStream("HiddenClassVarHandleMarkHolder.class")) { + markHolderClassData = classData.readAllBytes(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + ByteBuffer expectedMaxEvents = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN); + expectedMaxEvents.putInt(HiddenClassVarHandleMarkHolder.MAX_EVENTS); + byte[] maxEvents = expectedMaxEvents.array(); + maxEventsOffsets = findOffsets(markHolderClassData, maxEvents); + if (maxEventsOffsets.length != 2) { + throw new RuntimeException("hop"); + } + + ByteBuffer expectedMaxEventsMask = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN); + expectedMaxEventsMask.putLong(HiddenClassVarHandleMarkHolder.MAX_EVENTS_MASK); + byte[] maxEventsMax = expectedMaxEventsMask.array(); + maxEventsMaskOffsets = findOffsets(markHolderClassData, maxEventsMax); + if (maxEventsMaskOffsets.length != 1) { + throw new RuntimeException("skip"); + } + + replaceSize(markHolderClassData, DEFAULT_SIZE); + } + + @Override + public MarkHolder create(long markHolderId) { + return create(markHolderId, DEFAULT_SIZE); + } + + MarkHolder create(long markHolderId, int size) { + final byte[] classData; + if (size != DEFAULT_SIZE) { + classData = Arrays.copyOf(markHolderClassData, markHolderClassData.length); + replaceSize(classData, size); + } else { + classData = markHolderClassData; + } + try { + Class<?> clz = MethodHandles.lookup().defineHiddenClass(classData, true).lookupClass(); + return clz.asSubclass(MarkHolder.class).getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private void replaceSize(byte[] haystack, int size) { + ByteBuffer buf = ByteBuffer.wrap(haystack).order(ByteOrder.BIG_ENDIAN); + for (int off : maxEventsOffsets) { + buf.putInt(off, size); + } + for (int off : maxEventsMaskOffsets) { + buf.putLong(off, size - 1); + } + } + + private static int[] findOffsets(byte[] haystack, byte[] needle) { + int[] matches = new int[0]; + outer: for (int i = 0; i < haystack.length - needle.length; i++) { + for (int k = 0; k < needle.length; k++) { + if (haystack[i + k] != needle[k]) { + continue outer; + } + } + matches = Arrays.copyOf(matches, matches.length + 1); + matches[matches.length - 1] = i; + } + return matches; + } + } + + private SecretHiddenClassMarkHolderProvider() {} +} diff --git a/java15/src/test/java/io/perfmark/java15/HiddenClassVarHandleMarkHolderTest.java b/java15/src/test/java/io/perfmark/java15/HiddenClassVarHandleMarkHolderTest.java new file mode 100644 index 0000000..73200d5 --- /dev/null +++ b/java15/src/test/java/io/perfmark/java15/HiddenClassVarHandleMarkHolderTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java15; + +import static org.junit.Assert.assertEquals; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkHolder; +import io.perfmark.testing.MarkHolderTest; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class HiddenClassVarHandleMarkHolderTest extends MarkHolderTest { + + private final long gen = 1L << Generator.GEN_OFFSET; + + @Override + protected MarkHolder getMarkHolder() { + return new SecretHiddenClassMarkHolderProvider.HiddenClassMarkHolderProvider().create(1234); + } + + @Test + public void read_getsAllButLastIfNotWriter() { + MarkHolder mh = getMarkHolder(); + int events = mh.maxMarks() - 1; + for (int i = 0; i < events; i++) { + mh.start(gen, "task", 3); + } + + List<Mark> marks = mh.read(true); + assertEquals(events - 1, marks.size()); + } + + @Test + public void read_getsAllIfNotWriterButNoWrap() { + MarkHolder mh = getMarkHolder(); + + int events = mh.maxMarks() - 2; + for (int i = 0; i < events; i++) { + mh.start(gen, "task", 3); + } + + List<Mark> marks = mh.read(true); + assertEquals(events, marks.size()); + } +} diff --git a/java6/BUILD.bazel b/java6/BUILD.bazel new file mode 100644 index 0000000..eb1ac1a --- /dev/null +++ b/java6/BUILD.bazel @@ -0,0 +1,33 @@ +java_library( + name = "generator", + srcs = [ + "src/main/java/io/perfmark/java6/SecretVolatileGenerator.java", + ], + deps = [ + "//impl:generator", + ], +) + +java_library( + name = "mark-holder", + srcs = [ + "src/main/java/io/perfmark/java6/SynchronizedMarkHolder.java", + ], + deps = [ + "//impl:generator", + "//impl:mark", + "//impl:mark-holder", + ], +) + +java_library( + name = "mark-holder-provider", + srcs = [ + "src/main/java/io/perfmark/java6/SecretSynchronizedMarkHolderProvider.java", + ], + deps = [ + ":mark-holder", + "//impl:mark-holder", + "//impl:mark-holder-provider", + ], +) diff --git a/java6/build.gradle b/java6/build.gradle new file mode 100644 index 0000000..d4d5dbf --- /dev/null +++ b/java6/build.gradle @@ -0,0 +1,68 @@ +description = "PerfMark Java6 API" +ext.moduleName = "io.perfmark.javasix" +ext.jdkVersion = JavaVersion.VERSION_1_6 + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +compileJava { + sourceCompatibility = jdkVersion + targetCompatibility = jdkVersion + + options.compilerArgs.add("-Xlint:-options") +} + +sourceSets { + jmh {} +} + + +compileJmhJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + javaCompiler = javaToolchains.compilerFor({ + languageVersion = JavaLanguageVersion.of("17") + }) + options.errorprone.enabled = true + options.errorprone.excludedPaths.set(".*/build/generated/sources/annotationProcessor/.*") +} + +dependencies { + implementation project(':perfmark-impl') + compileOnly libs.jsr305 + + testImplementation(project(":perfmark-api")) + testImplementation(project(':perfmark-testing')) + + jmhImplementation project(':perfmark-api'), + project(':perfmark-impl'), + project(':perfmark-java6'), + project(':perfmark-testing') + jmhImplementation libs.junit + jmhImplementation libs.jmhcore + jmhAnnotationProcessor libs.jmhanno +} + +tasks.register('jmh', Test) { + description = 'Runs integration tests.' + group = 'stress' + + testClassesDirs = sourceSets.jmh.output.classesDirs + classpath = sourceSets.jmh.runtimeClasspath + + javaLauncher = javaToolchains.launcherFor({ + languageVersion = JavaLanguageVersion.of("17") + }) + //shouldRunAfter test +} + +javadoc { + exclude 'io/perfmark/java6**' +} + +jar { + exclude 'io/perfmark/java6/Internal*' +} diff --git a/java6/src/jmh/java/io/perfmark/java6/SynchronizedMarkHolderBenchmarkTest.java b/java6/src/jmh/java/io/perfmark/java6/SynchronizedMarkHolderBenchmarkTest.java new file mode 100644 index 0000000..2da17d1 --- /dev/null +++ b/java6/src/jmh/java/io/perfmark/java6/SynchronizedMarkHolderBenchmarkTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java6; + +import io.perfmark.impl.MarkHolder; +import io.perfmark.testing.MarkHolderBenchmark; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; + +@RunWith(JUnit4.class) +public class SynchronizedMarkHolderBenchmarkTest { + @Test + public void markHolderBenchmark() throws Exception { + Options options = new OptionsBuilder() + .include(SynchronizedMarkHolderBenchmark.class.getCanonicalName()) + .addProfiler("perfasm") + .measurementIterations(5) + .warmupIterations(10) + .forks(1) + .warmupTime(TimeValue.seconds(1)) + .measurementTime(TimeValue.seconds(1)) + .shouldFailOnError(true) + + // This is necessary to run in the IDE, otherwise it would inherit the VM args. + .jvmArgs("-da", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+LogCompilation", + "-XX:LogFile=/dev/null", + "-XX:+PrintAssembly", + "-XX:+PrintInterpreter", + "-XX:+PrintNMethods", + "-XX:+PrintNativeNMethods", + "-XX:+PrintSignatureHandlers", + "-XX:+PrintAdapterHandlers", + "-XX:+PrintStubCode", + "-XX:+PrintCompilation", + "-XX:+PrintInlining", + "-XX:PrintAssemblyOptions=syntax", + "-XX:PrintAssemblyOptions=intel") + .build(); + + new Runner(options).run(); + } + + @State(Scope.Thread) + public static class SynchronizedMarkHolderBenchmark extends MarkHolderBenchmark { + @Override + public MarkHolder getMarkHolder() { + return new SynchronizedMarkHolder(16384); + } + } +} diff --git a/java6/src/jmh/java/io/perfmark/java6/VolatileGeneratorBenchmarkTest.java b/java6/src/jmh/java/io/perfmark/java6/VolatileGeneratorBenchmarkTest.java new file mode 100644 index 0000000..de7aa6d --- /dev/null +++ b/java6/src/jmh/java/io/perfmark/java6/VolatileGeneratorBenchmarkTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java6; + +import io.perfmark.impl.Generator; +import io.perfmark.testing.GeneratorBenchmark; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; + +@RunWith(JUnit4.class) +public class VolatileGeneratorBenchmarkTest { + + @Test + public void generatorBenchmark() throws Exception { + Options options = new OptionsBuilder() + .include(VolatileGeneratorBenchmark.class.getCanonicalName()) + .measurementIterations(5) + .warmupIterations(10) + .forks(1) + .warmupTime(TimeValue.seconds(1)) + .measurementTime(TimeValue.seconds(1)) + .shouldFailOnError(true) + // This is necessary to run in the IDE, otherwise it would inherit the VM args. + .jvmArgs("-da") + .build(); + + new Runner(options).run(); + } + + @State(Scope.Benchmark) + public static class VolatileGeneratorBenchmark extends GeneratorBenchmark { + @Override + protected Generator getGenerator() { + return new SecretVolatileGenerator.VolatileGenerator(); + } + } +} diff --git a/java6/src/main/java/io/perfmark/java6/Internal.java b/java6/src/main/java/io/perfmark/java6/Internal.java new file mode 100644 index 0000000..9b7115b --- /dev/null +++ b/java6/src/main/java/io/perfmark/java6/Internal.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java6; + +public final class Internal { + private Internal() { + throw new AssertionError("nope"); + } +} diff --git a/java6/src/main/java/io/perfmark/java6/SecretSynchronizedMarkHolderProvider.java b/java6/src/main/java/io/perfmark/java6/SecretSynchronizedMarkHolderProvider.java new file mode 100644 index 0000000..356820d --- /dev/null +++ b/java6/src/main/java/io/perfmark/java6/SecretSynchronizedMarkHolderProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java6; + +import io.perfmark.impl.MarkHolder; +import io.perfmark.impl.MarkHolderProvider; + +final class SecretSynchronizedMarkHolderProvider { + + public static final class SynchronizedMarkHolderProvider extends MarkHolderProvider { + + public SynchronizedMarkHolderProvider() {} + + @Override + @SuppressWarnings("deprecation") + public MarkHolder create() { + return new SynchronizedMarkHolder(); + } + + @Override + public MarkHolder create(long markHolderId) { + return new SynchronizedMarkHolder(); + } + } + + private SecretSynchronizedMarkHolderProvider() { + throw new AssertionError("nope"); + } +} diff --git a/java6/src/main/java/io/perfmark/java6/SecretVolatileGenerator.java b/java6/src/main/java/io/perfmark/java6/SecretVolatileGenerator.java new file mode 100644 index 0000000..efeb0da --- /dev/null +++ b/java6/src/main/java/io/perfmark/java6/SecretVolatileGenerator.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java6; + +import io.perfmark.impl.Generator; + +final class SecretVolatileGenerator { + + // @UsedReflectively + public static final class VolatileGenerator extends Generator { + + // @UsedReflectively + public VolatileGenerator() {} + + private volatile long gen; + + @Override + public void setGeneration(long generation) { + gen = generation; + } + + @Override + public long getGeneration() { + return gen; + } + + @Override + public long costOfGetNanos() { + return 3; + } + + @Override + public long costOfSetNanos() { + return 10; + } + } + + private SecretVolatileGenerator() { + throw new AssertionError("nope"); + } +} diff --git a/java6/src/main/java/io/perfmark/java6/SynchronizedMarkHolder.java b/java6/src/main/java/io/perfmark/java6/SynchronizedMarkHolder.java new file mode 100644 index 0000000..4b4afeb --- /dev/null +++ b/java6/src/main/java/io/perfmark/java6/SynchronizedMarkHolder.java @@ -0,0 +1,399 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java6; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkHolder; +import java.util.AbstractCollection; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; + +final class SynchronizedMarkHolder extends MarkHolder { + private static final long GEN_MASK = (1 << Generator.GEN_OFFSET) - 1; + + private static final long START_N1S1_OP = Mark.Operation.TASK_START_N1S1.ordinal(); + private static final long START_N1S2_OP = Mark.Operation.TASK_START_N1S2.ordinal(); + private static final long STOP_N1S0_OP = Mark.Operation.TASK_END_N1S0.ordinal(); + private static final long STOP_N1S1_OP = Mark.Operation.TASK_END_N1S1.ordinal(); + private static final long STOP_N1S2_OP = Mark.Operation.TASK_END_N1S2.ordinal(); + private static final long EVENT_N1S1_OP = Mark.Operation.EVENT_N1S1.ordinal(); + private static final long EVENT_N1S2_OP = Mark.Operation.EVENT_N1S2.ordinal(); + private static final long EVENT_N2S2_OP = Mark.Operation.EVENT_N2S2.ordinal(); + private static final long LINK_OP = Mark.Operation.LINK.ordinal(); + private static final long TAG_N1S1_OP = Mark.Operation.TAG_N1S1.ordinal(); + private static final long TAG_KEYED_N0S2_OP = Mark.Operation.TAG_KEYED_N0S2.ordinal(); + private static final long TAG_KEYED_N1S1_OP = Mark.Operation.TAG_KEYED_N1S1.ordinal(); + private static final long TAG_KEYED_N2S1_OP = Mark.Operation.TAG_KEYED_N2S1.ordinal(); + + private final int maxEvents; + private final long maxEventsMask; + + // where to write to next + private long nIdx; + private long sIdx; + + private final long[] nums; + private final String[] strings; + + SynchronizedMarkHolder() { + this(32768); + } + + SynchronizedMarkHolder(int maxEvents) { + if (((maxEvents - 1) & maxEvents) != 0) { + throw new IllegalArgumentException(maxEvents + " is not a power of two"); + } + if (maxEvents <= 0) { + throw new IllegalArgumentException(maxEvents + " is not positive"); + } + this.maxEvents = maxEvents; + this.maxEventsMask = maxEvents - 1L; + this.nums = new long[maxEvents]; + this.strings = new String[maxEvents]; + } + + private void writeNnss(long genOp, long n0, long n1, String s0, String s1) { + nums[(int) (nIdx++ & maxEventsMask)] = n0; + nums[(int) (nIdx++ & maxEventsMask)] = n1; + strings[(int) (sIdx++ & maxEventsMask)] = s0; + strings[(int) (sIdx++ & maxEventsMask)] = s1; + nums[(int) (nIdx++ & maxEventsMask)] = genOp; + } + + private void writeNss(long genOp, long n0, String s0, String s1) { + nums[(int) (nIdx++ & maxEventsMask)] = n0; + strings[(int) (sIdx++ & maxEventsMask)] = s0; + strings[(int) (sIdx++ & maxEventsMask)] = s1; + nums[(int) (nIdx++ & maxEventsMask)] = genOp; + } + + private void writeNs(long genOp, long n0, String s0) { + nums[(int) (nIdx++ & maxEventsMask)] = n0; + strings[(int) (sIdx++ & maxEventsMask)] = s0; + nums[(int) (nIdx++ & maxEventsMask)] = genOp; + } + + private void writeN(long genOp, long n0) { + nums[(int) (nIdx++ & maxEventsMask)] = n0; + nums[(int) (nIdx++ & maxEventsMask)] = genOp; + } + + private void writeNns(long genOp, long n0, long n1, String s0) { + nums[(int) (nIdx++ & maxEventsMask)] = n0; + nums[(int) (nIdx++ & maxEventsMask)] = n1; + strings[(int) (sIdx++ & maxEventsMask)] = s0; + nums[(int) (nIdx++ & maxEventsMask)] = genOp; + } + + private void writeSs(long genOp, String s0, String s1) { + strings[(int) (sIdx++ & maxEventsMask)] = s0; + strings[(int) (sIdx++ & maxEventsMask)] = s1; + nums[(int) (nIdx++ & maxEventsMask)] = genOp; + } + + @Override + public synchronized void start( + long gen, String taskName, String tagName, long tagId, long nanoTime) { + writeNs(gen + START_N1S1_OP, nanoTime, taskName); + writeNs(gen + TAG_N1S1_OP, tagId, tagName); + } + + @Override + public synchronized void start(long gen, String taskName, long nanoTime) { + writeNs(gen + START_N1S1_OP, nanoTime, taskName); + } + + @Override + public synchronized void start(long gen, String taskName, String subTaskName, long nanoTime) { + writeNss(gen + START_N1S2_OP, nanoTime, taskName, subTaskName); + } + + @Override + public synchronized void link(long gen, long linkId) { + writeN(gen + LINK_OP, linkId); + } + + @Override + public synchronized void stop(long gen, long nanoTime) { + writeN(gen + STOP_N1S0_OP, nanoTime); + } + + @Override + public synchronized void stop( + long gen, String taskName, String tagName, long tagId, long nanoTime) { + writeNs(gen + TAG_N1S1_OP, tagId, tagName); + writeNs(gen + STOP_N1S1_OP, nanoTime, taskName); + } + + @Override + public synchronized void stop(long gen, String taskName, long nanoTime) { + writeNs(gen + STOP_N1S1_OP, nanoTime, taskName); + } + + @Override + public synchronized void stop(long gen, String taskName, String subTaskName, long nanoTime) { + writeNss(gen + STOP_N1S2_OP, nanoTime, taskName, subTaskName); + } + + @Override + public synchronized void event( + long gen, String eventName, String tagName, long tagId, long nanoTime) { + writeNnss(gen + EVENT_N2S2_OP, nanoTime, tagId, eventName, tagName); + } + + @Override + public synchronized void event(long gen, String eventName, long nanoTime) { + writeNs(gen + EVENT_N1S1_OP, nanoTime, eventName); + } + + @Override + public synchronized void event(long gen, String eventName, String subEventName, long nanoTime) { + writeNss(gen + EVENT_N1S2_OP, nanoTime, eventName, subEventName); + } + + @Override + public synchronized void attachTag(long gen, String tagName, long tagId) { + writeNs(gen + TAG_N1S1_OP, tagId, tagName); + } + + @Override + public synchronized void attachKeyedTag(long gen, String name, long value0) { + writeNs(gen + TAG_KEYED_N1S1_OP, value0, name); + } + + @Override + public synchronized void attachKeyedTag(long gen, String name, String value) { + writeSs(gen + TAG_KEYED_N0S2_OP, name, value); + } + + @Override + public synchronized void attachKeyedTag(long gen, String name, long value0, long value1) { + writeNns(gen + TAG_KEYED_N2S1_OP, value0, value1, name); + } + + @Override + public synchronized void resetForTest() { + Arrays.fill(nums, 0); + Arrays.fill(strings, null); + nIdx = 0; + sIdx = 0; + } + + @Override + public List<Mark> read(boolean concurrentWrites) { + Kyoo<Long> numQ; + Kyoo<String> stringQ; + { + final long[] nums = new long[maxEvents]; + final String[] strings = new String[maxEvents]; + final long nIdx; + final long sIdx; + + synchronized (this) { + System.arraycopy(this.nums, 0, nums, 0, maxEvents); + System.arraycopy(this.strings, 0, strings, 0, maxEvents); + nIdx = this.nIdx; + sIdx = this.sIdx; + } + Long[] numsBoxed = new Long[nums.length]; + for (int i = 0; i < nums.length; i++) { + numsBoxed[i] = nums[i]; + } + numQ = new Kyoo<Long>(numsBoxed, nIdx, (int) Math.min(nIdx, maxEvents)); + stringQ = new Kyoo<String>(strings, sIdx, (int) Math.min(sIdx, maxEvents)); + } + + Deque<Mark> marks = new ArrayDeque<Mark>(maxEvents); + + while (true) { + if (numQ.isEmpty()) { + break; + } + long genOp = numQ.remove(); + long gen = genOp & ~GEN_MASK; + Mark.Operation op = Mark.Operation.valueOf((int) (genOp & GEN_MASK)); + + if (op.getNumbers() > numQ.size() + || op.getStrings() > stringQ.size()) { + break; + } + long n1; + String s1; + long n2; + String s2; + switch (op) { + case TASK_START_N1S1: + n1 = numQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.taskStart(gen, n1, s1)); + break; + case TASK_START_N1S2: + n1 = numQ.remove(); + s2 = stringQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.taskStart(gen, n1, s1, s2)); + break; + case TASK_END_N1S0: + n1 = numQ.remove(); + marks.addFirst(Mark.taskEnd(gen, n1)); + break; + case TASK_END_N1S1: + n1 = numQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.taskEnd(gen, n1, s1)); + break; + case TASK_END_N1S2: + n1 = numQ.remove(); + s2 = stringQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.taskEnd(gen, n1, s1, s2)); + break; + case EVENT_N1S1: + n1 = numQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.event(gen, n1, s1)); + break; + case EVENT_N1S2: + n1 = numQ.remove(); + s2 = stringQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.event(gen, n1, s1, s2)); + break; + case EVENT_N2S2: + n2 = numQ.remove(); + s2 = stringQ.remove(); + n1 = numQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.event(gen, n1, s1, s2, n2)); + break; + case EVENT_N2S3: + throw new UnsupportedOperationException(); + case LINK: + n1 = numQ.remove(); + marks.addFirst(Mark.link(gen, n1)); + break; + case TAG_N0S1: + throw new UnsupportedOperationException(); + case TAG_N1S0: + throw new UnsupportedOperationException(); + case TAG_N1S1: + n1 = numQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.tag(gen, s1, n1)); + break; + case TAG_KEYED_N0S2: + s2 = stringQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.keyedTag(gen, s1, s2)); + break; + case TAG_KEYED_N1S1: + n1 = numQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.keyedTag(gen, s1, n1)); + break; + case TAG_KEYED_N2S1: + n2 = numQ.remove(); + n1 = numQ.remove(); + s1 = stringQ.remove(); + marks.addFirst(Mark.keyedTag(gen, s1, n1, n2)); + break; + case NONE: + throw new UnsupportedOperationException(); + } + } + + return Collections.unmodifiableList(new ArrayList<Mark>(marks)); + } + + @Override + public int maxMarks() { + return maxEvents; + } + + private final class Kyoo<T> extends AbstractCollection<T> implements Queue<T> { + + private final T[] elements; + private final long wIdx; + private final int size; + + private int ri; + + Kyoo(T[] elements, long wIdx, int size) { + this.elements = elements; + this.wIdx = wIdx; + this.size = size; + } + + @Override + public Iterator<T> iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return size - ri; + } + + @Override + public boolean offer(T t) { + throw new UnsupportedOperationException(); + } + + @Override + public T remove() { + checkSize(); + return poll(); + } + + @Override + public T poll() { + if (size() == 0) { + return null; + } + int rIdx = (int) (((wIdx - 1) - ri++) & maxEventsMask); + return elements[rIdx]; + } + + @Override + public T element() { + checkSize(); + return peek(); + } + + @Override + public T peek() { + if (size() == 0) { + return null; + } + int rIdx = (int) (((wIdx - 1) - ri) & maxEventsMask); + return elements[rIdx]; + } + + private void checkSize() { + if (size() == 0) { + throw new IllegalStateException(); + } + } + } +} diff --git a/java6/src/main/java/io/perfmark/java6/package-info.java b/java6/src/main/java/io/perfmark/java6/package-info.java new file mode 100644 index 0000000..824dad2 --- /dev/null +++ b/java6/src/main/java/io/perfmark/java6/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +@javax.annotation.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +package io.perfmark.java6; diff --git a/java6/src/test/java/io/perfmark/java6/AutoLoadTest.java b/java6/src/test/java/io/perfmark/java6/AutoLoadTest.java new file mode 100644 index 0000000..97841be --- /dev/null +++ b/java6/src/test/java/io/perfmark/java6/AutoLoadTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.java6; + +import static org.junit.Assert.assertEquals; + +import io.perfmark.PerfMark; +import io.perfmark.impl.MarkList; +import io.perfmark.impl.Storage; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Checks that auto loading this provider works. + */ +@RunWith(JUnit4.class) +public class AutoLoadTest { + @Test + public void autoLoad() { + Storage.clearLocalStorage(); + PerfMark.setEnabled(true); + PerfMark.startTask("hi"); + PerfMark.stopTask("hi"); + PerfMark.setEnabled(false); + MarkList markList = Storage.readForTest(); + assertEquals(2, markList.size()); + } +} diff --git a/java6/src/test/java/io/perfmark/java6/SynchronizedMarkHolderTest.java b/java6/src/test/java/io/perfmark/java6/SynchronizedMarkHolderTest.java new file mode 100644 index 0000000..9f1ad68 --- /dev/null +++ b/java6/src/test/java/io/perfmark/java6/SynchronizedMarkHolderTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java6; + +import io.perfmark.impl.MarkHolder; +import io.perfmark.testing.MarkHolderTest; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SynchronizedMarkHolderTest extends MarkHolderTest { + + @Override + protected MarkHolder getMarkHolder() { + return new SynchronizedMarkHolder(); + } +} diff --git a/java6/src/test/java/io/perfmark/java6/VersionTest.java b/java6/src/test/java/io/perfmark/java6/VersionTest.java new file mode 100644 index 0000000..a6ef6ba --- /dev/null +++ b/java6/src/test/java/io/perfmark/java6/VersionTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.java6; + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class VersionTest { + + private static final short JAVA_VERSION_6 = 50; + + @Test + public void blah() throws Exception { + Class<?> clz = SecretSynchronizedMarkHolderProvider.class; + try (InputStream stream = + clz.getClassLoader().getResourceAsStream(clz.getName().replace('.', '/') + ".class")) { + byte[] data = stream.readAllBytes(); + ByteBuffer buf = ByteBuffer.wrap(data); + // Discard magic int + buf.getInt(); + short major = buf.getShort(); + short minor = buf.getShort(); + assertEquals(0, major); + assertEquals(JAVA_VERSION_6, minor); + } + } +} diff --git a/java7/BUILD.bazel b/java7/BUILD.bazel new file mode 100644 index 0000000..7c98084 --- /dev/null +++ b/java7/BUILD.bazel @@ -0,0 +1,9 @@ +java_library( + name = "generator", + srcs = [ + "src/main/java/io/perfmark/java7/SecretMethodHandleGenerator.java", + ], + deps = [ + "//impl:generator", + ], +) diff --git a/java7/build.gradle b/java7/build.gradle new file mode 100644 index 0000000..6173dd4 --- /dev/null +++ b/java7/build.gradle @@ -0,0 +1,59 @@ + +description = "PerfMark Java7 API" +ext.moduleName = "io.perfmark.javaseven" +ext.jdkVersion = JavaVersion.VERSION_1_7 + +compileJava { + sourceCompatibility = jdkVersion + targetCompatibility = jdkVersion + + options.compilerArgs.add("-Xlint:-options") +} + +sourceSets { + jmh {} +} + + +compileJmhJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + javaCompiler = javaToolchains.compilerFor({ + languageVersion = JavaLanguageVersion.of("11") + }) + options.errorprone.excludedPaths.set(".*/build/generated/sources/annotationProcessor/.*") +} + +dependencies { + implementation project(':perfmark-impl') + compileOnly libs.jsr305 + + jmhImplementation project(':perfmark-api'), + project(':perfmark-impl'), + project(':perfmark-java7'), + project(':perfmark-testing') + jmhImplementation libs.junit + jmhImplementation libs.jmhcore + jmhAnnotationProcessor libs.jmhanno +} + +javadoc { + exclude 'io/perfmark/java7**' +} + +jar { + exclude 'io/perfmark/java7/Internal*' +} + +tasks.register('jmh', Test) { + description = 'Runs integration tests.' + group = 'stress' + + testClassesDirs = sourceSets.jmh.output.classesDirs + classpath = sourceSets.jmh.runtimeClasspath + + javaLauncher = javaToolchains.launcherFor({ + languageVersion = JavaLanguageVersion.of("16") + }) + //shouldRunAfter test +}
\ No newline at end of file diff --git a/java7/src/jmh/java/io/perfmark/java7/MethodHandleGeneratorBenchmarkTest.java b/java7/src/jmh/java/io/perfmark/java7/MethodHandleGeneratorBenchmarkTest.java new file mode 100644 index 0000000..e10285f --- /dev/null +++ b/java7/src/jmh/java/io/perfmark/java7/MethodHandleGeneratorBenchmarkTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java7; + +import io.perfmark.impl.Generator; +import io.perfmark.testing.GeneratorBenchmark; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; +import org.openjdk.jmh.runner.options.VerboseMode; + +@RunWith(JUnit4.class) +public class MethodHandleGeneratorBenchmarkTest { + + @Test + public void generatorBenchmark() throws Exception { + Options options = new OptionsBuilder() + .include(MethodHandleGeneratorBenchmark.class.getCanonicalName()) + .measurementIterations(5) + .warmupIterations(10) + .forks(1) + .verbosity(VerboseMode.EXTRA) + .warmupTime(TimeValue.seconds(1)) + .measurementTime(TimeValue.seconds(1)) + .shouldFailOnError(true) + // This is necessary to run in the IDE, otherwise it would inherit the VM args. + .jvmArgs("-da") + .build(); + + new Runner(options).run(); + } + + @State(Scope.Benchmark) + public static class MethodHandleGeneratorBenchmark extends GeneratorBenchmark { + @Override + protected Generator getGenerator() { + return new SecretMethodHandleGenerator.MethodHandleGenerator(); + } + } +} diff --git a/java7/src/main/java/io/perfmark/java7/Internal.java b/java7/src/main/java/io/perfmark/java7/Internal.java new file mode 100644 index 0000000..5578e2b --- /dev/null +++ b/java7/src/main/java/io/perfmark/java7/Internal.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java7; + +public final class Internal { + + private Internal() { + throw new AssertionError("nope"); + } +} diff --git a/java7/src/main/java/io/perfmark/java7/SecretMethodHandleGenerator.java b/java7/src/main/java/io/perfmark/java7/SecretMethodHandleGenerator.java new file mode 100644 index 0000000..1ab5d36 --- /dev/null +++ b/java7/src/main/java/io/perfmark/java7/SecretMethodHandleGenerator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java7; + +import io.perfmark.impl.Generator; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MutableCallSite; + +final class SecretMethodHandleGenerator { + + // UsedReflectively + public static final class MethodHandleGenerator extends Generator { + private static final MutableCallSite currentGeneration = + new MutableCallSite(MethodHandles.constant(long.class, 0)); + private static final MutableCallSite[] currentGenerations = + new MutableCallSite[] {currentGeneration}; + private static final MethodHandle currentGenerationGetter = currentGeneration.dynamicInvoker(); + + public MethodHandleGenerator() {} + + @Override + public long getGeneration() { + try { + return (long) currentGenerationGetter.invoke(); + } catch (Throwable throwable) { + return FAILURE; + } + } + + @Override + public void setGeneration(long generation) { + currentGeneration.setTarget(MethodHandles.constant(long.class, generation)); + MutableCallSite.syncAll(currentGenerations); + } + + @Override + public long costOfGetNanos() { + // Method handles compile to constants, so this is effectively free. + // JMH testing on a Skylake x86_64 processor shows the cost to be about 0.3ns. + return 0; + } + + @Override + public long costOfSetNanos() { + // based on JMH testing on a Skylake x86_64 processor. + return 2_000_000; + } + } + + private SecretMethodHandleGenerator() { + throw new AssertionError("nope"); + } +} diff --git a/java7/src/main/java/io/perfmark/java7/package-info.java b/java7/src/main/java/io/perfmark/java7/package-info.java new file mode 100644 index 0000000..0878c1e --- /dev/null +++ b/java7/src/main/java/io/perfmark/java7/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +@javax.annotation.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +package io.perfmark.java7; diff --git a/java7/src/test/java/io/perfmark/java7/VersionTest.java b/java7/src/test/java/io/perfmark/java7/VersionTest.java new file mode 100644 index 0000000..03b65aa --- /dev/null +++ b/java7/src/test/java/io/perfmark/java7/VersionTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.java7; + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class VersionTest { + + private static final short JAVA_VERSION_7 = 51; + + @Test + public void blah() throws Exception { + Class<?> clz = SecretMethodHandleGenerator.class; + try (InputStream stream = + clz.getClassLoader().getResourceAsStream(clz.getName().replace('.', '/') + ".class")) { + byte[] data = stream.readAllBytes(); + ByteBuffer buf = ByteBuffer.wrap(data); + // Discard magic int + buf.getInt(); + short major = buf.getShort(); + short minor = buf.getShort(); + assertEquals(0, major); + assertEquals(JAVA_VERSION_7, minor); + } + } +} diff --git a/java9/BUILD.bazel b/java9/BUILD.bazel new file mode 100644 index 0000000..7a0fc5f --- /dev/null +++ b/java9/BUILD.bazel @@ -0,0 +1,34 @@ +java_library( + name = "generator", + srcs = [ + "src/main/java/io/perfmark/java9/SecretVarHandleGenerator.java", + ], + deps = [ + "//impl:generator", + ], +) + +java_library( + name = "mark-holder", + srcs = [ + "src/main/java/io/perfmark/java9/VarHandleMarkHolder.java", + ], + deps = [ + "//impl:generator", + "//impl:mark", + "//impl:mark-holder", + ], +) + +java_library( + name = "mark-holder-provider", + srcs = [ + "src/main/java/io/perfmark/java9/SecretVarHandleMarkHolderProvider.java", + ], + deps = [ + ":mark-holder", + "//impl:generator", + "//impl:mark-holder", + "//impl:mark-holder-provider", + ], +) diff --git a/java9/build.gradle.kts b/java9/build.gradle.kts new file mode 100644 index 0000000..5a163a4 --- /dev/null +++ b/java9/build.gradle.kts @@ -0,0 +1,88 @@ +import net.ltgt.gradle.errorprone.errorprone + +plugins { + id("io.github.reyerizo.gradle.jcstress") +} + +buildscript { + extra.apply{ + set("moduleName", "io.perfmark.javanine") + } +} + +val jdkVersion = JavaVersion.VERSION_1_9 + +description = "PerfMark Java9 API" + +sourceSets { + create("jmh") +} + +val jmhImplementation by configurations.getting { + extendsFrom(configurations.implementation.get()) +} + +val jmhAnnotationProcessor by configurations.getting { + extendsFrom(configurations.annotationProcessor.get()) +} + +dependencies { + implementation(project(":perfmark-impl")) + compileOnly(libs.jsr305) + + testImplementation(project(":perfmark-api")) + testImplementation(project(":perfmark-testing")) + jcstressImplementation(project(":perfmark-impl")) + + jmhImplementation(project(":perfmark-api")) + jmhImplementation(project(":perfmark-impl")) + jmhImplementation(project(":perfmark-java9")) + jmhImplementation(project(":perfmark-testing")) + jmhImplementation(libs.junit) + jmhImplementation(libs.jmhcore) + jmhAnnotationProcessor(libs.jmhanno) +} + +tasks.named<JavaCompile>("compileJava") { + sourceCompatibility = jdkVersion.toString() + targetCompatibility = jdkVersion.toString() +} + +tasks.named<JavaCompile>("compileJmhJava") { + sourceCompatibility = JavaVersion.VERSION_11.toString() + targetCompatibility = JavaVersion.VERSION_11.toString() + options.errorprone.excludedPaths.set(".*/build/generated/sources/annotationProcessor/.*") + +} + +tasks.register<Test>("jmh") { + description = "Runs integration tests." + group = "stress" + + testClassesDirs = sourceSets["jmh"].output.classesDirs + classpath = sourceSets["jmh"].runtimeClasspath + + javaLauncher.set(javaToolchains.launcherFor({ + languageVersion.set(JavaLanguageVersion.of("11")) + })) +} + + +tasks.named<Jar>("jar") { + exclude("io/perfmark/java9/Internal*") +} + +tasks.named<Javadoc>("javadoc") { + exclude("io/perfmark/java9/**") +} + +// ./gradlew --no-daemon clean :perfmark-java9:jcstress +jcstress { + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.5" + // mode "tough" + deoptRatio = "2" +} + +tasks.named<JavaCompile>("compileJcstressJava") { + options.errorprone.excludedPaths.set(".*/build/generated/sources/annotationProcessor/.*") +} diff --git a/java9/src/jcstress/java/io/perfmark/java9/PerfMarkStorageStress.java b/java9/src/jcstress/java/io/perfmark/java9/PerfMarkStorageStress.java new file mode 100644 index 0000000..5c80cb9 --- /dev/null +++ b/java9/src/jcstress/java/io/perfmark/java9/PerfMarkStorageStress.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java9; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import java.util.List; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Description; +import org.openjdk.jcstress.annotations.Expect; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.L_Result; + +/** Simulates the PerfMarkStorage racy reader. */ +@JCStressTest +@Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "0 Writes") +@Outcome(id = "1", expect = Expect.ACCEPTABLE, desc = "1 Write") +@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "2 Writes") +@Outcome(id = "3", expect = Expect.ACCEPTABLE, desc = "3 Writes") +@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "4 Writes") +@Outcome(id = "5", expect = Expect.ACCEPTABLE, desc = "5 Writes") +@Outcome(id = "6", expect = Expect.ACCEPTABLE, desc = "6 Writes") +@Outcome(id = "7", expect = Expect.ACCEPTABLE, desc = "7 Writes") +@Outcome(id = "8", expect = Expect.ACCEPTABLE, desc = "8 Writes") +@Outcome(id = "9", expect = Expect.ACCEPTABLE, desc = "9 Writes") +@Outcome(id = "10", expect = Expect.ACCEPTABLE, desc = "10 Writes") +@Outcome(id = "11", expect = Expect.ACCEPTABLE, desc = "11 Writes") +@Outcome(id = "12", expect = Expect.ACCEPTABLE, desc = "12 Writes") +@Outcome(id = "13", expect = Expect.ACCEPTABLE, desc = "13 Writes") +@Outcome(id = "14", expect = Expect.ACCEPTABLE, desc = "14 Writes") +@Outcome(id = "15", expect = Expect.ACCEPTABLE, desc = "15 Writes") +@Outcome(id = "16", expect = Expect.ACCEPTABLE, desc = "16 Writes") +@Outcome(id = "17", expect = Expect.ACCEPTABLE, desc = "17 Writes") +@Outcome(id = "18", expect = Expect.ACCEPTABLE, desc = "18 Writes") +@Outcome(id = "19", expect = Expect.ACCEPTABLE, desc = "19 Writes") +@Outcome(id = "20", expect = Expect.ACCEPTABLE, desc = "20 Writes") +@Outcome(id = "21", expect = Expect.ACCEPTABLE, desc = "21 Writes") +@Outcome(id = "22", expect = Expect.ACCEPTABLE, desc = "22 Writes") +@Outcome(id = "23", expect = Expect.ACCEPTABLE, desc = "23 Writes") +@Outcome(id = "24", expect = Expect.ACCEPTABLE, desc = "24 Writes") +@Outcome(id = "25", expect = Expect.ACCEPTABLE, desc = "25 Writes") +@Outcome(id = "26", expect = Expect.ACCEPTABLE, desc = "26 Writes") +@Outcome(id = "27", expect = Expect.ACCEPTABLE, desc = "27 Writes") +@Outcome(id = "28", expect = Expect.ACCEPTABLE, desc = "28 Writes") +@Outcome(id = "29", expect = Expect.ACCEPTABLE, desc = "29 Writes") +@Outcome(id = "30", expect = Expect.ACCEPTABLE, desc = "30 Writes") +@Outcome(id = "31", expect = Expect.ACCEPTABLE, desc = "31 Writes") +@Outcome(id = "32", expect = Expect.ACCEPTABLE, desc = "32 Writes") +@Outcome(id = "-1", expect = Expect.FORBIDDEN, desc = "Wrong Type") +@Outcome(id = "-3", expect = Expect.FORBIDDEN, desc = "Wrong ID") +@State +@Description("Simulates the PerfMarkStorage reader.") +public class PerfMarkStorageStress { + private static final int OFFSET; + private static final int SIZE = 32; + + static { + OFFSET = 31; + assert Generator.GEN_OFFSET <= OFFSET; + } + + private final VarHandleMarkHolder holder = new VarHandleMarkHolder(SIZE); + + @Actor + public void writer() { + for (long i = 0; i < SIZE * 4; i++) { + holder.link(i << OFFSET, i); + } + } + + @Actor + @SuppressWarnings("ReferenceEquality") + public void reader(L_Result r) { + List<Mark> marks = holder.read(true); + int ret = marks.size(); + for (int i = 0; i < marks.size(); i++) { + Mark mark = marks.get(i); + if (mark.getOperation() != Mark.Operation.LINK) { + ret = -1; + break; + } else if (mark.getGeneration() >>> OFFSET != mark.getLinkId()) { + ret = -2; + break; + } else { + // keep going + } + } + r.r1 = ret; + } +} diff --git a/java9/src/jmh/java/io/perfmark/java9/VarHandleGeneratorBenchmarkTest.java b/java9/src/jmh/java/io/perfmark/java9/VarHandleGeneratorBenchmarkTest.java new file mode 100644 index 0000000..b16267a --- /dev/null +++ b/java9/src/jmh/java/io/perfmark/java9/VarHandleGeneratorBenchmarkTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java9; + +import io.perfmark.impl.Generator; +import io.perfmark.testing.GarbageCollector; +import io.perfmark.testing.GeneratorBenchmark; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.GroupThreads; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; +import org.openjdk.jmh.runner.options.VerboseMode; + +@RunWith(JUnit4.class) +public class VarHandleGeneratorBenchmarkTest { + + @Test + public void generatorBenchmark() throws Exception { + Options options = new OptionsBuilder() + .include(VarHandleGeneratorBenchmark.class.getCanonicalName()) + .measurementIterations(5) + .warmupIterations(10) + .forks(1) + .verbosity(VerboseMode.EXTRA) + .warmupTime(TimeValue.seconds(1)) + .measurementTime(TimeValue.seconds(1)) + .shouldFailOnError(true) + // This is necessary to run in the IDE, otherwise it would inherit the VM args. + .jvmArgs("-da") + .build(); + + new Runner(options).run(); + } + + @State(Scope.Benchmark) + public static class VarHandleGeneratorBenchmark extends GeneratorBenchmark { + @Override + protected Generator getGenerator() { + return new SecretVarHandleGenerator.VarHandleGenerator(); + } + } +} diff --git a/java9/src/jmh/java/io/perfmark/java9/VarHandleMarkHolderBenchmarkTest.java b/java9/src/jmh/java/io/perfmark/java9/VarHandleMarkHolderBenchmarkTest.java new file mode 100644 index 0000000..b6162fe --- /dev/null +++ b/java9/src/jmh/java/io/perfmark/java9/VarHandleMarkHolderBenchmarkTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.java9; + +import static java.util.List.of; + +import io.perfmark.impl.MarkHolder; +import io.perfmark.testing.GarbageCollector; +import io.perfmark.testing.MarkHolderBenchmark; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; + +@RunWith(Parameterized.class) +public class VarHandleMarkHolderBenchmarkTest { + + @Parameterized.Parameter(0) + public GarbageCollector gc; + + @Parameterized.Parameters + public static Collection<Object[]> args() { + List<Object[]> cases = new ArrayList<>(); + for (GarbageCollector gc : GarbageCollector.values()) { + if (gc != GarbageCollector.CMS) { + continue; + } + cases.add(List.of(gc).toArray(new Object[0])); + } + + return List.copyOf(cases); + } + + @Test + public void markHolderBenchmark() throws Exception { + List<String> jvmArgs = new ArrayList<>(); + jvmArgs.add("-da"); + jvmArgs.addAll(gc.jvmArgs()); + Options options = new OptionsBuilder() + .include(VarHandleMarkHolderBenchmark.class.getCanonicalName()) + .measurementIterations(10) + .warmupIterations(10) + .forks(1) + .warmupTime(TimeValue.seconds(1)) + .measurementTime(TimeValue.seconds(1)) + .param("GC", gc.name()) + .shouldFailOnError(true) + // This is necessary to run in the IDE, otherwise it would inherit the VM args. + .jvmArgs(jvmArgs.toArray(new String[0])) + .build(); + + new Runner(options).run(); + } + + @State(Scope.Thread) + public static class VarHandleMarkHolderBenchmark extends MarkHolderBenchmark { + @Override + public MarkHolder getMarkHolder() { + return new VarHandleMarkHolder(16384); + } + } +} diff --git a/java9/src/main/java/io/perfmark/java9/Internal.java b/java9/src/main/java/io/perfmark/java9/Internal.java new file mode 100644 index 0000000..f914736 --- /dev/null +++ b/java9/src/main/java/io/perfmark/java9/Internal.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java9; + +public final class Internal { + private Internal() { + throw new AssertionError("nope"); + } +} diff --git a/java9/src/main/java/io/perfmark/java9/SecretVarHandleGenerator.java b/java9/src/main/java/io/perfmark/java9/SecretVarHandleGenerator.java new file mode 100644 index 0000000..e59ef44 --- /dev/null +++ b/java9/src/main/java/io/perfmark/java9/SecretVarHandleGenerator.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java9; + +import io.perfmark.impl.Generator; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; + +final class SecretVarHandleGenerator { + + /** + * This class let's PerfMark have fairly low overhead detection if it is enabled, with reasonable + * time between enabled and other threads noticing. Since this uses Java 9 APIs, it may not be + * available. + */ + public static final class VarHandleGenerator extends Generator { + + private static final VarHandle GEN; + + static { + try { + GEN = MethodHandles.lookup().findVarHandle(VarHandleGenerator.class, "gen", long.class); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public VarHandleGenerator() {} + + @SuppressWarnings("unused") + private long gen; + + @Override + public void setGeneration(long generation) { + GEN.setRelease(this, generation); + } + + @Override + public long getGeneration() { + return (long) GEN.getAcquire(this); + } + + @Override + public long costOfSetNanos() { + return 3; + } + + @Override + public long costOfGetNanos() { + return 2; + } + } + + private SecretVarHandleGenerator() {} +} diff --git a/java9/src/main/java/io/perfmark/java9/SecretVarHandleMarkHolderProvider.java b/java9/src/main/java/io/perfmark/java9/SecretVarHandleMarkHolderProvider.java new file mode 100644 index 0000000..171e84c --- /dev/null +++ b/java9/src/main/java/io/perfmark/java9/SecretVarHandleMarkHolderProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java9; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.MarkHolder; +import io.perfmark.impl.MarkHolderProvider; + +final class SecretVarHandleMarkHolderProvider { + + public static final class VarHandleMarkHolderProvider extends MarkHolderProvider { + + public VarHandleMarkHolderProvider() { + // Do some basic operations to see if it works. + MarkHolder holder = create(12345); + holder.start(1 << Generator.GEN_OFFSET, "bogus", 0); + holder.stop(1 << Generator.GEN_OFFSET, "bogus", 0); + int size = holder.read(false).size(); + if (size != 2) { + throw new AssertionError("Wrong size " + size); + } + } + + @Override + @SuppressWarnings("deprecation") + public MarkHolder create() { + return new VarHandleMarkHolder(); + } + + @Override + public MarkHolder create(long markHolderId) { + return new VarHandleMarkHolder(); + } + } + + private SecretVarHandleMarkHolderProvider() { + throw new AssertionError("nope"); + } +} diff --git a/java9/src/main/java/io/perfmark/java9/VarHandleMarkHolder.java b/java9/src/main/java/io/perfmark/java9/VarHandleMarkHolder.java new file mode 100644 index 0000000..523663b --- /dev/null +++ b/java9/src/main/java/io/perfmark/java9/VarHandleMarkHolder.java @@ -0,0 +1,388 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java9; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkHolder; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Deque; +import java.util.List; + +/** VarHandleMarkHolder is a MarkHolder optimized for wait free writes and few reads. */ +final class VarHandleMarkHolder extends MarkHolder { + private static final long GEN_MASK = (1 << Generator.GEN_OFFSET) - 1; + private static final long START_OP = 1; // Mark.Operation.TASK_START.ordinal(); + private static final long START_S_OP = 2; + private static final long START_T_OP = 3; // Mark.Operation.TASK_START_T.ordinal(); + private static final long STOP_OP = 4; // Mark.Operation.TASK_END.ordinal(); + private static final long STOP_V_OP = 5; + private static final long STOP_T_OP = 6; // Mark.Operation.TASK_END_T.ordinal(); + private static final long STOP_S_OP = 7; + private static final long EVENT_OP = 8; // Mark.Operation.EVENT.ordinal(); + private static final long EVENT_T_OP = 9; // Mark.Operation.EVENT_T.ordinal(); + private static final long EVENT_S_OP = 10; + private static final long LINK_OP = 11; // Mark.Operation.LINK.ordinal(); + private static final long ATTACH_T_OP = 12; // Mark.Operation.ATTACH_TAG.ordinal(); + private static final long ATTACH_SS_OP = 13; + private static final long ATTACH_SN_OP = 14; + private static final long ATTACH_SNN_OP = 15; + + private static final VarHandle IDX; + private static final VarHandle STRINGS; + private static final VarHandle LONGS; + + static { + try { + IDX = MethodHandles.lookup().findVarHandle(VarHandleMarkHolder.class, "idx", long.class); + STRINGS = MethodHandles.arrayElementVarHandle(String[].class); + LONGS = MethodHandles.arrayElementVarHandle(long[].class); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private final int maxEvents; + private final long maxEventsMax; + + // where to write to next + @SuppressWarnings("unused") // Used Reflectively + private volatile long idx; + + private final String[] taskNames; + private final String[] tagNames; + private final long[] tagIds; + private final long[] nanoTimes; + private final long[] genOps; + + VarHandleMarkHolder() { + this(32768); + } + + VarHandleMarkHolder(int maxEvents) { + if (((maxEvents - 1) & maxEvents) != 0) { + throw new IllegalArgumentException(maxEvents + " is not a power of two"); + } + if (maxEvents <= 0) { + throw new IllegalArgumentException(maxEvents + " is not positive"); + } + this.maxEvents = maxEvents; + this.maxEventsMax = maxEvents - 1L; + this.taskNames = new String[maxEvents]; + this.tagNames = new String[maxEvents]; + this.tagIds = new long[maxEvents]; + this.nanoTimes = new long[maxEvents]; + this.genOps = new long[maxEvents]; + } + + @Override + public void start(long gen, String taskName, String tagName, long tagId, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + START_T_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void start(long gen, String taskName, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, taskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + START_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void start(long gen, String taskName, String subTaskName, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, subTaskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + START_S_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void link(long gen, long linkId) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + LONGS.setOpaque(tagIds, i, linkId); + LONGS.setOpaque(genOps, i, gen + LINK_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_V_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, String taskName, String tagName, long tagId, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_T_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, String taskName, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, taskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void stop(long gen, String taskName, String subTaskName, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, taskName); + STRINGS.setOpaque(tagNames, i, subTaskName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + STOP_S_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void event(long gen, String eventName, String tagName, long tagId, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, eventName); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + EVENT_T_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void event(long gen, String eventName, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, eventName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + EVENT_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void event(long gen, String eventName, String subEventName, long nanoTime) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(taskNames, i, eventName); + STRINGS.setOpaque(tagNames, i, subEventName); + LONGS.setOpaque(nanoTimes, i, nanoTime); + LONGS.setOpaque(genOps, i, gen + EVENT_S_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachTag(long gen, String tagName, long tagId) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(tagNames, i, tagName); + LONGS.setOpaque(tagIds, i, tagId); + LONGS.setOpaque(genOps, i, gen + ATTACH_T_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachKeyedTag(long gen, String name, long value) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(tagNames, i, name); + LONGS.setOpaque(tagIds, i, value); + LONGS.setOpaque(genOps, i, gen + ATTACH_SN_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachKeyedTag(long gen, String name, long value0, long value1) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(tagNames, i, name); + LONGS.setOpaque(tagIds, i, value0); + LONGS.setOpaque(nanoTimes, i, value1); + LONGS.setOpaque(genOps, i, gen + ATTACH_SNN_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void attachKeyedTag(long gen, String name, String value) { + long localIdx = (long) IDX.get(this); + int i = (int) (localIdx & maxEventsMax); + STRINGS.setOpaque(tagNames, i, name); + STRINGS.setOpaque(taskNames, i, value); + LONGS.setOpaque(genOps, i, gen + ATTACH_SS_OP); + IDX.setRelease(this, localIdx + 1); + VarHandle.storeStoreFence(); + } + + @Override + public void resetForTest() { + Arrays.fill(taskNames, null); + Arrays.fill(tagNames, null); + Arrays.fill(tagIds, 0); + Arrays.fill(nanoTimes, 0); + Arrays.fill(genOps, 0); + IDX.setRelease(this, 0L); + VarHandle.storeStoreFence(); + } + + @Override + public List<Mark> read(boolean concurrentWrites) { + final String[] localTaskNames = new String[maxEvents]; + final String[] localTagNames = new String[maxEvents]; + final long[] localTagIds = new long[maxEvents]; + final long[] localNanoTimes = new long[maxEvents]; + final long[] localGenOps = new long[maxEvents]; + long startIdx = (long) IDX.getOpaque(this); + VarHandle.loadLoadFence(); + int size = (int) Math.min(startIdx, maxEvents); + for (int i = 0; i < size; i++) { + localTaskNames[i] = (String) STRINGS.getOpaque(taskNames, i); + localTagNames[i] = (String) STRINGS.getOpaque(tagNames, i); + localTagIds[i] = (long) LONGS.getOpaque(tagIds, i); + localNanoTimes[i] = (long) LONGS.getOpaque(nanoTimes, i); + localGenOps[i] = (long) LONGS.getOpaque(genOps, i); + } + VarHandle.loadLoadFence(); + long endIdx = (long) IDX.getOpaque(this); + if (endIdx < startIdx) { + throw new AssertionError(); + } + // If we are reading from ourselves (such as in a test), we can assume there isn't an in + // progress write modifying the oldest entry. Additionally, if the writer has not yet + // wrapped around, the last entry cannot have been corrupted. + boolean tailValid = !concurrentWrites || endIdx < maxEvents - 1; + endIdx += !tailValid ? 1 : 0; + long eventsToDrop = endIdx - startIdx; + final Deque<Mark> marks = new ArrayDeque<>(size); + for (int i = 0; i < size - eventsToDrop; i++) { + int readIdx = (int) ((startIdx - i - 1) & maxEventsMax); + long gen = localGenOps[readIdx] & ~GEN_MASK; + int opVal = (int) (localGenOps[readIdx] & GEN_MASK); + switch (opVal) { + case (int) START_T_OP: + marks.addFirst(Mark.tag(gen, localTagNames[readIdx], localTagIds[readIdx])); + // fallthrough + case (int) START_OP: + marks.addFirst(Mark.taskStart(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + break; + case (int) START_S_OP: + marks.addFirst( + Mark.taskStart( + gen, localNanoTimes[readIdx], localTaskNames[readIdx], localTagNames[readIdx])); + break; + case (int) STOP_V_OP: + marks.addFirst(Mark.taskEnd(gen, localNanoTimes[readIdx])); + break; + case (int) STOP_S_OP: + marks.addFirst( + Mark.taskEnd( + gen, localNanoTimes[readIdx], localTaskNames[readIdx], localTagNames[readIdx])); + break; + case (int) STOP_OP: + marks.addFirst(Mark.taskEnd(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + break; + case (int) STOP_T_OP: + marks.addFirst(Mark.taskEnd(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + marks.addFirst(Mark.tag(gen, localTagNames[readIdx], localTagIds[readIdx])); + break; + case (int) EVENT_OP: + marks.addFirst(Mark.event(gen, localNanoTimes[readIdx], localTaskNames[readIdx])); + break; + case (int) EVENT_T_OP: + marks.addFirst( + Mark.event( + gen, + localNanoTimes[readIdx], + localTaskNames[readIdx], + localTagNames[readIdx], + localTagIds[readIdx])); + break; + case (int) EVENT_S_OP: + marks.addFirst( + Mark.event( + gen, localNanoTimes[readIdx], localTaskNames[readIdx], localTagNames[readIdx])); + break; + case (int) LINK_OP: + marks.addFirst(Mark.link(gen, localTagIds[readIdx])); + break; + case (int) ATTACH_T_OP: + marks.addFirst(Mark.tag(gen, localTagNames[readIdx], localTagIds[readIdx])); + break; + case (int) ATTACH_SS_OP: + marks.addFirst(Mark.keyedTag(gen, localTagNames[readIdx], localTaskNames[readIdx])); + break; + case (int) ATTACH_SN_OP: + marks.addFirst(Mark.keyedTag(gen, localTagNames[readIdx], localTagIds[readIdx])); + break; + case (int) ATTACH_SNN_OP: + marks.addFirst( + Mark.keyedTag( + gen, localTagNames[readIdx], localTagIds[readIdx], localNanoTimes[readIdx])); + break; + default: + throw new ConcurrentModificationException("Read of storage was not threadsafe " + opVal); + } + } + + return Collections.unmodifiableList(new ArrayList<>(marks)); + } + + @Override + public int maxMarks() { + return maxEvents; + } +} diff --git a/java9/src/main/java/io/perfmark/java9/package-info.java b/java9/src/main/java/io/perfmark/java9/package-info.java new file mode 100644 index 0000000..da6c957 --- /dev/null +++ b/java9/src/main/java/io/perfmark/java9/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019 Google LLC + * + * 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 9+ enabled generator and mark-holder. */ +@javax.annotation.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +package io.perfmark.java9; diff --git a/java9/src/test/java/io/perfmark/java9/AutoLoadTest.java b/java9/src/test/java/io/perfmark/java9/AutoLoadTest.java new file mode 100644 index 0000000..3d2e020 --- /dev/null +++ b/java9/src/test/java/io/perfmark/java9/AutoLoadTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.java9; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import io.perfmark.PerfMark; +import io.perfmark.impl.MarkList; +import io.perfmark.impl.Storage; +import java.lang.reflect.Field; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Checks that auto loading this provider works. + */ +@RunWith(JUnit4.class) +public class AutoLoadTest { + @Test + public void autoLoad() throws Exception { + Storage.clearLocalStorage(); + PerfMark.setEnabled(true); + PerfMark.startTask("hi"); + PerfMark.stopTask("hi"); + PerfMark.setEnabled(false); + MarkList markList = Storage.readForTest(); + assertEquals(2, markList.size()); + + // Have to check after to ensure it loaded properly + Field field = Storage.class.getDeclaredField("markHolderProvider"); + field.setAccessible(true); + + assertTrue(field.get(null) instanceof SecretVarHandleMarkHolderProvider.VarHandleMarkHolderProvider); + } +} diff --git a/java9/src/test/java/io/perfmark/java9/PerfMarkStressTest.java b/java9/src/test/java/io/perfmark/java9/PerfMarkStressTest.java new file mode 100644 index 0000000..28d416b --- /dev/null +++ b/java9/src/test/java/io/perfmark/java9/PerfMarkStressTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java9; + +import io.perfmark.Link; +import io.perfmark.PerfMark; +import io.perfmark.Tag; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.RecursiveTask; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class PerfMarkStressTest { + + @Test + public void fibonacci() { + ForkJoinPool fjp = new ForkJoinPool(8); + final class Fibonacci extends RecursiveTask<Long> { + + private final long input; + private final Link link; + + Fibonacci(long input, Link link) { + this.input = input; + this.link = link; + } + + @Override + protected Long compute() { + Tag tag = PerfMark.createTag(input); + PerfMark.startTask("compute", tag); + PerfMark.linkIn(link); + try { + if (input >= 20) { + Link link2 = PerfMark.linkOut(); + ForkJoinTask<Long> task1 = new Fibonacci(input - 1, link2).fork(); + Fibonacci task2 = new Fibonacci(input - 2, link2); + return task2.compute() + task1.join(); + } else { + return computeUnboxed(input); + } + } finally { + PerfMark.stopTask("compute", tag); + } + } + + private long computeUnboxed(long n) { + if (n <= 1) { + return n; + } + return computeUnboxed(n - 1) + computeUnboxed(n - 2); + } + } + PerfMark.setEnabled(true); + PerfMark.startTask("calc"); + Link link = PerfMark.linkOut(); + ForkJoinTask<Long> task = new Fibonacci(30, link); + fjp.execute(task); + PerfMark.stopTask("calc"); + Long res; + try { + res = task.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + System.err.println(res); + + fjp.shutdown(); + } +} diff --git a/java9/src/test/java/io/perfmark/java9/VarHandleMarkHolderTest.java b/java9/src/test/java/io/perfmark/java9/VarHandleMarkHolderTest.java new file mode 100644 index 0000000..06afb8d --- /dev/null +++ b/java9/src/test/java/io/perfmark/java9/VarHandleMarkHolderTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.java9; + +import static org.junit.Assert.assertEquals; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkHolder; +import io.perfmark.testing.MarkHolderTest; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class VarHandleMarkHolderTest extends MarkHolderTest { + + private final long gen = 1L << Generator.GEN_OFFSET; + + @Override + protected MarkHolder getMarkHolder() { + return new VarHandleMarkHolder(); + } + + @Test + public void read_getsAllButLastIfNotWriter() { + MarkHolder mh = getMarkHolder(); + int events = mh.maxMarks() - 1; + for (int i = 0; i < events; i++) { + mh.start(gen, "task", 3); + } + + List<Mark> marks = mh.read(true); + assertEquals(events - 1, marks.size()); + } + + @Test + public void read_getsAllIfNotWriterButNoWrap() { + MarkHolder mh = getMarkHolder(); + + int events = mh.maxMarks() - 2; + for (int i = 0; i < events; i++) { + mh.start(gen, "task", 3); + } + + List<Mark> marks = mh.read(true); + assertEquals(events, marks.size()); + } +} diff --git a/java9/src/test/java/io/perfmark/java9/VersionTest.java b/java9/src/test/java/io/perfmark/java9/VersionTest.java new file mode 100644 index 0000000..4dd650b --- /dev/null +++ b/java9/src/test/java/io/perfmark/java9/VersionTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * 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 io.perfmark.java9; + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class VersionTest { + + private static final short JAVA_VERSION_9 = 53; + + @Test + public void blah() throws Exception { + Class<?> clz = SecretVarHandleGenerator.class; + try (InputStream stream = + clz.getClassLoader().getResourceAsStream(clz.getName().replace('.', '/') + ".class")) { + byte[] data = stream.readAllBytes(); + ByteBuffer buf = ByteBuffer.wrap(data); + // Discard magic int + buf.getInt(); + short major = buf.getShort(); + short minor = buf.getShort(); + assertEquals(0, major); + assertEquals(JAVA_VERSION_9, minor); + } + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..65d32e1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,43 @@ +rootProject.name = 'perfmark' +include ":perfmark-agent" +include ":perfmark-api" +include ":perfmark-api-testing" +include ":perfmark-examples" +include ":perfmark-impl" +include ":perfmark-java6" +include ":perfmark-java7" +include ":perfmark-java9" +include ":perfmark-java15" +include ":perfmark-testing" +include ":perfmark-tracewriter" +include ":perfmark-traceviewer" + +project(':perfmark-agent').projectDir = "$rootDir/agent" as File +project(':perfmark-api').projectDir = "$rootDir/api" as File +project(':perfmark-api-testing').projectDir = "$rootDir/api/testing" as File +project(':perfmark-examples').projectDir = "$rootDir/examples" as File +project(':perfmark-impl').projectDir = "$rootDir/impl" as File +project(':perfmark-java6').projectDir = "$rootDir/java6" as File +project(':perfmark-java7').projectDir = "$rootDir/java7" as File +project(':perfmark-java9').projectDir = "$rootDir/java9" as File +project(':perfmark-java15').projectDir = "$rootDir/java15" as File +project(':perfmark-testing').projectDir = "$rootDir/testing" as File +project(':perfmark-tracewriter').projectDir = "$rootDir/tracewriter" as File +project(':perfmark-traceviewer').projectDir = "$rootDir/traceviewer" as File + +dependencyResolutionManagement { + versionCatalogs { + libs { + version("jmh", "1.35") + + library('junit', 'junit:junit:4.13.2') + library('jsr305', 'com.google.code.findbugs:jsr305:3.0.2') + library('errorprone', 'com.google.errorprone:error_prone_annotations:2.16') + library('truth', 'com.google.truth:truth:1.1.3') + + library('jmhcore', 'org.openjdk.jmh', 'jmh-core').versionRef('jmh') + library('jmhanno', 'org.openjdk.jmh', 'jmh-generator-annprocess').versionRef('jmh') + + } + } +} diff --git a/testing/build.gradle.kts b/testing/build.gradle.kts new file mode 100644 index 0000000..d3c5a7c --- /dev/null +++ b/testing/build.gradle.kts @@ -0,0 +1,25 @@ +buildscript { + extra.apply{ + set("moduleName", "io.perfmark.testing") + } +} + +val jdkVersion = JavaVersion.VERSION_11 + +description = "PerfMark Testing" + +dependencies { + implementation(project(":perfmark-api")) + implementation(project(":perfmark-impl")) + implementation(libs.jmhcore) + implementation(libs.junit) +} + +tasks.named<JavaCompile>("compileJava") { + sourceCompatibility = jdkVersion.toString() + targetCompatibility = jdkVersion.toString() +} + +tasks.named<Javadoc>("javadoc") { + exclude("io/perfmark/testing/**") +} diff --git a/testing/src/main/java/io/perfmark/testing/GarbageCollector.java b/testing/src/main/java/io/perfmark/testing/GarbageCollector.java new file mode 100644 index 0000000..8309b91 --- /dev/null +++ b/testing/src/main/java/io/perfmark/testing/GarbageCollector.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.testing; + +import java.util.List; + +@SuppressWarnings("ImmutableEnumChecker") +public enum GarbageCollector { + G1("-XX:+UseG1GC"), + ZGC("-XX:+UseZGC"), + SHENANDOAH("-XX:+UseShenandoahGC"), + SERIAL("-XX:+UseSerialGC"), + PARALLEL("-XX:+UseParallelGC"), + EPSILON("-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"), + CMS("-XX:+UseConcMarkSweepGC"), + ; + + private final List<String> jvmArgs; + + GarbageCollector(String ... jvmArgs) { + this.jvmArgs = List.of(jvmArgs); + } + + public List<String> jvmArgs() { + return jvmArgs; + } +} diff --git a/testing/src/main/java/io/perfmark/testing/GeneratorBenchmark.java b/testing/src/main/java/io/perfmark/testing/GeneratorBenchmark.java new file mode 100644 index 0000000..fd3eca5 --- /dev/null +++ b/testing/src/main/java/io/perfmark/testing/GeneratorBenchmark.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.testing; + +import io.perfmark.impl.Generator; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.GroupThreads; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +@State(Scope.Benchmark) +public class GeneratorBenchmark { + + private Generator generator; + + @Setup(Level.Trial) + public void setUp() { + generator = getGenerator(); + generator.setGeneration(Generator.FAILURE); + } + + protected Generator getGenerator() { + throw new UnsupportedOperationException(); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void ifEnabled() { + if (isEnabled(getGeneration())) { + Blackhole.consumeCPU(1000); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public long getGeneration() { + return generator.getGeneration(); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public long getAndSetAndGetGeneration() { + long oldGeneration = generator.getGeneration(); + generator.setGeneration(oldGeneration + 1); + return generator.getGeneration(); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @GroupThreads(3) + public long racyGetAndSetAndGetGeneration() { + long oldGeneration = generator.getGeneration(); + generator.setGeneration(oldGeneration + 1); + return generator.getGeneration(); + } + + protected static boolean isEnabled(long gen) { + return ((gen >>> Generator.GEN_OFFSET) & 0x1L) != 0L; + } +} diff --git a/testing/src/main/java/io/perfmark/testing/MarkHolderBenchmark.java b/testing/src/main/java/io/perfmark/testing/MarkHolderBenchmark.java new file mode 100644 index 0000000..7699f91 --- /dev/null +++ b/testing/src/main/java/io/perfmark/testing/MarkHolderBenchmark.java @@ -0,0 +1,142 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.testing; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.MarkHolder; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +@State(Scope.Thread) +public class MarkHolderBenchmark { + + private static final long gen = 1 << Generator.GEN_OFFSET; + private static final String taskName = "hiya"; + + public static final List<String> ASM_FLAGS = List.of( + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+LogCompilation", + "-XX:LogFile=/tmp/blah.txt", + "-XX:+PrintAssembly", + "-XX:+PrintInterpreter", + "-XX:+PrintNMethods", + "-XX:+PrintNativeNMethods", + "-XX:+PrintSignatureHandlers", + "-XX:+PrintAdapterHandlers", + "-XX:+PrintStubCode", + "-XX:+PrintCompilation", + "-XX:+PrintInlining", + "-XX:PrintAssemblyOptions=syntax", + "-XX:PrintAssemblyOptions=intel"); + + protected MarkHolder markHolder; + + private String tagName = "tag"; + private long tagId = 0xf0f0; + private long nanoTime = 0xf1f1; + private long linkId = 0xf2f2; + + public MarkHolder getMarkHolder() { + throw new UnsupportedOperationException("not implemented"); + } + + @Setup + public final void setUp() { + markHolder = getMarkHolder(); + } + + @Param({"G1"}) + GarbageCollector GC; + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void start_name_tag() { + markHolder.start(gen, taskName, tagName, tagId, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void start_name_noTag() { + markHolder.start(gen, taskName, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void start_name_subname() { + markHolder.start(gen, taskName, taskName, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void stop_name_tag() { + markHolder.stop(gen, taskName, tagName, tagId, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void stop_name_noTag() { + markHolder.stop(gen, taskName, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void stop_name_subname() { + markHolder.stop(gen, taskName, tagName, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void link() { + markHolder.link(gen, linkId); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void event_name_tag() { + markHolder.event(gen, taskName, tagName, tagId, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void event_name_noTag() { + markHolder.event(gen, taskName, nanoTime); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void event_name_subname() { + markHolder.event(gen, taskName, taskName, nanoTime); + } +} diff --git a/testing/src/main/java/io/perfmark/testing/MarkHolderTest.java b/testing/src/main/java/io/perfmark/testing/MarkHolderTest.java new file mode 100644 index 0000000..cb471d8 --- /dev/null +++ b/testing/src/main/java/io/perfmark/testing/MarkHolderTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.testing; + +import static org.junit.Assert.assertEquals; + +import io.perfmark.impl.Generator; +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkHolder; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Base class for Mark holders. + * + * <p>This should really not be a super class, but JUnit4 makes doing this hard. + */ +@RunWith(JUnit4.class) +public abstract class MarkHolderTest { + + private final long gen = 1L << Generator.GEN_OFFSET; + + protected MarkHolder getMarkHolder() { + throw new UnsupportedOperationException("not implemented"); + } + + @Test + public void taskTagStartStop() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task", 3); + mh.stop(gen, "task", 4); + + List<Mark> marks = mh.read(false); + assertEquals(2, marks.size()); + List<Mark> expected = List.of(Mark.taskStart(gen, 3, "task"), Mark.taskEnd(gen, 4, "task")); + assertEquals(expected, marks); + } + + @Test + public void taskTagStartStop_subTask() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task", "subtask", 3); + mh.stop(gen, "task", "subtask", 4); + + List<Mark> marks = mh.read(false); + assertEquals(2, marks.size()); + List<Mark> expected = List.of( + Mark.taskStart(gen, 3, "task", "subtask"), Mark.taskEnd(gen, 4, "task", "subtask")); + assertEquals(expected, marks); + } + + @Test + public void taskTagStartStop_tag() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task", "tag", 9, 3); + mh.stop(gen, "task", "tag", 9, 4); + + List<Mark> marks = mh.read(false); + assertEquals(4, marks.size()); + List<Mark> expected = + List.of( + Mark.taskStart(gen, 3, "task"), + Mark.tag(gen, "tag", 9), + Mark.tag(gen, "tag", 9), + Mark.taskEnd(gen, 4, "task")); + assertEquals(expected, marks); + } + + @Test + public void taskStartStartStopStop() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task1", 3); + mh.start(gen, "task2", 4); + mh.start(gen, "task3", 5); + mh.stop(gen, 6); + mh.stop(gen, "task2", 7); + mh.stop(gen, "task1", 8); + + List<Mark> marks = mh.read(false); + + assertEquals(6, marks.size()); + List<Mark> expected = + List.of( + Mark.taskStart(gen, 3, "task1"), + Mark.taskStart(gen, 4, "task2"), + Mark.taskStart(gen, 5, "task3"), + Mark.taskEnd(gen, 6), + Mark.taskEnd(gen, 7, "task2"), + Mark.taskEnd(gen, 8, "task1")); + assertEquals(expected, marks); + } + + @Test + public void attachTag() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task", 3); + mh.attachTag(gen, "tag", 8); + mh.stop(gen, "task", 4); + + List<Mark> marks = mh.read(false); + assertEquals(3, marks.size()); + List<Mark> expected = + List.of( + Mark.taskStart(gen, 3, "task"), Mark.tag(gen, "tag", 8), Mark.taskEnd(gen, 4, "task")); + assertEquals(expected, marks); + } + + @Test + public void attachKeyedTag() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task", 3); + mh.attachKeyedTag(gen, "key1", 8); + mh.attachKeyedTag(gen, "key2", 8, 9); + mh.attachKeyedTag(gen, "key3", "value"); + mh.stop(gen, "task", 4); + + List<Mark> marks = mh.read(false); + assertEquals(5, marks.size()); + List<Mark> expected = + List.of( + Mark.taskStart(gen, 3, "task"), + Mark.keyedTag(gen, "key1", 8), + Mark.keyedTag(gen, "key2", 8, 9), + Mark.keyedTag(gen, "key3", "value"), + Mark.taskEnd(gen, 4, "task")); + assertEquals(expected, marks); + } + + @Test + public void event() { + MarkHolder mh = getMarkHolder(); + mh.event(gen, "task1", 8); + mh.event(gen, "task2", 5); + + List<Mark> marks = mh.read(false); + + assertEquals(2, marks.size()); + List<Mark> expected = List.of(Mark.event(gen, 8, "task1"), Mark.event(gen, 5, "task2")); + assertEquals(expected, marks); + } + + @Test + public void event_tag() { + MarkHolder mh = getMarkHolder(); + mh.event(gen, "task1", "tag1", 7, 8); + mh.event(gen, "task2", "tag2", 6, 5); + + List<Mark> marks = mh.read(false); + + assertEquals(2, marks.size()); + List<Mark> expected = + List.of( + Mark.event(gen, 8, "task1", "tag1", 7), Mark.event(gen, 5, "task2", "tag2", 6)); + assertEquals(expected, marks); + } + + @Test + public void event_subevent() { + MarkHolder mh = getMarkHolder(); + mh.event(gen, "task1", "subtask3", 8); + mh.event(gen, "task2", "subtask4", 5); + + List<Mark> marks = mh.read(false); + + assertEquals(2, marks.size()); + List<Mark> expected = + List.of( + Mark.event(gen, 8, "task1", "subtask3"), Mark.event(gen, 5, "task2", "subtask4")); + assertEquals(expected, marks); + } + + @Test + public void linkInLinkOut() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task1", 3); + mh.link(gen, 9); + mh.link(gen, -9); + mh.stop(gen, "task1", 4); + + List<Mark> marks = mh.read(false); + + assertEquals(marks.size(), 4); + List<Mark> expected = + List.of( + Mark.taskStart(gen, 3, "task1"), + Mark.link(gen, 9), + Mark.link(gen, -9), + Mark.taskEnd(gen, 4, "task1")); + assertEquals(expected, marks); + } + + @Test + public void read_getsAllIfWriter() { + MarkHolder mh = getMarkHolder(); + mh.start(gen, "task", 3); + + List<Mark> marks = mh.read(false); + + assertEquals(marks.size(), 1); + } +} diff --git a/traceviewer/BUILD.bazel b/traceviewer/BUILD.bazel new file mode 100644 index 0000000..c05fd11 --- /dev/null +++ b/traceviewer/BUILD.bazel @@ -0,0 +1,12 @@ +java_library( + name = "tracewriter", + srcs = glob([ + "src/main/java/io/perfmark/traceviewer/*.java", + ]), + visibility = ["//visibility:public"], + deps = [ + "//tracewriter", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/traceviewer/build.gradle.kts b/traceviewer/build.gradle.kts new file mode 100644 index 0000000..850d0e9 --- /dev/null +++ b/traceviewer/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + extra.apply{ + set("moduleName", "io.perfmark.traceviewer") + } +} + +description = "PerfMark Trace Viewer" + +val jdkVersion = JavaVersion.VERSION_1_8 + +dependencies { + compileOnly(libs.jsr305) + compileOnly(libs.errorprone) + implementation(project(":perfmark-tracewriter")) + testImplementation(project(":perfmark-api")) +} + +tasks.getByName<JavaCompile>("compileJava") { + sourceCompatibility = jdkVersion.toString() + targetCompatibility = jdkVersion.toString() + options.compilerArgs.add("-Xlint:-options") +} diff --git a/traceviewer/src/main/java/io/perfmark/traceviewer/TraceEventViewer.java b/traceviewer/src/main/java/io/perfmark/traceviewer/TraceEventViewer.java new file mode 100644 index 0000000..e37b9b4 --- /dev/null +++ b/traceviewer/src/main/java/io/perfmark/traceviewer/TraceEventViewer.java @@ -0,0 +1,226 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.traceviewer; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.perfmark.tracewriter.TraceEventWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Base64; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * This class converts from the Trace Event json data into a full HTML page. It includes the trace + * viewer from Catapult, the Chromium trace UI. + * + * <p>This class is separate from {@link TraceEventWriter}, because it includes a fairly large HTML + * chunk, and brings in a differently licenced piece of code. + * + * <p>This code is <strong>NOT</strong> API stable, and may be removed in the future, or changed + * without notice. + * + * @since 0.17.0 + */ +public final class TraceEventViewer { + private static final Logger logger = Logger.getLogger(TraceEventViewer.class.getName()); + + // Copied from trace2html.html in the Catapult tracing code. + private static final String INLINE_TRACE_DATA = + "" + + " const traces = [];\n" + + " const viewerDataScripts = Polymer.dom(document).querySelectorAll(\n" + + " '#viewer-data');\n" + + " for (let i = 0; i < viewerDataScripts.length; i++) {\n" + + " let text = Polymer.dom(viewerDataScripts[i]).textContent;\n" + + " // Trim leading newlines off the text. They happen during writing.\n" + + " while (text[0] === '\\n') {\n" + + " text = text.substring(1);\n" + + " }\n" + + " onResult(tr.b.Base64.atob(text));\n" + + " viewer.updateDocumentFavicon();\n" + + " viewer.globalMode = true;\n" + + " viewer.viewTitle = document.title;\n" + + " break;\n" + + " }\n"; + + /** + * A convenience function around {@link #writeTraceHtml(Writer)}. This writes the trace data to a + * temporary file and logs the output location. + * + * @return the Path of the written file. + * @throws IOException if it can't write to the destination. + */ + @CanIgnoreReturnValue + public static Path writeTraceHtml() throws IOException { + Path path = Files.createTempFile("perfmark-trace-", ".html"); + try (OutputStream os = Files.newOutputStream(path, TRUNCATE_EXISTING); + Writer w = new OutputStreamWriter(os, UTF_8)) { + writeTraceHtml(w); + } + logger.log(Level.INFO, "Wrote PerfMark Trace file://{0}", new Object[] {path.toAbsolutePath()}); + return path; + } + + /** + * Writes all available trace data as a single HTML file into the given writer. + * + * @param writer The destination to write all HTML to. + * @throws IOException if it can't write to the writer. + */ + public static void writeTraceHtml(Writer writer) throws IOException { + InputStream indexStream = + TraceEventViewer.class.getResourceAsStream("third_party/catapult/index.html"); + if (indexStream == null) { + throw new IOException("unable to find index.html"); + } + String index = readAll(indexStream); + + InputStream webComponentsStream = + TraceEventViewer.class.getResourceAsStream("third_party/polymer/webcomponents.min.js"); + if (webComponentsStream == null) { + throw new IOException("unable to find webcomponents.min.js"); + } + String webComponents = readAll(webComponentsStream); + + InputStream traceViewerStream = + TraceEventViewer.class.getResourceAsStream("third_party/catapult/trace_viewer_full.html"); + if (traceViewerStream == null) { + throw new IOException("unable to find trace_viewer_full.html"); + } + String traceViewer = trimTraceViewer(readAll(traceViewerStream)); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (OutputStreamWriter w = new OutputStreamWriter(baos, UTF_8)) { + TraceEventWriter.writeTraceEvents(w); + } + byte[] traceData64 = Base64.getEncoder().encode(baos.toByteArray()); + + String indexWithWebComponents = replaceIndexWebComponents(index, webComponents); + + String indexWithTraceViewer = + replaceIndexTraceImport( + indexWithWebComponents, traceViewer, new String(traceData64, UTF_8)); + + String fullIndex = replaceIndexTraceData(indexWithTraceViewer, INLINE_TRACE_DATA); + writer.write(fullIndex); + writer.flush(); + } + + /** + * Replaces the normal {@code <link>} tag in index.html with a custom replacement, and optionally + * the inlined Trace data as a base64 script. This is because the trace2html.html file imports the + * data as a top level text/plain script. + */ + private static String replaceIndexTraceImport( + String index, String replacement, @Nullable String inlineTraceData64) { + int start = index.indexOf("IO_PERFMARK_TRACE_IMPORT"); + if (start == -1) { + throw new IllegalArgumentException("index doesn't contain IO_PERFMARK_TRACE_IMPORT"); + } + int line0pos = index.lastIndexOf('\n', start); + assert line0pos != -1; + int line1pos = index.indexOf('\n', line0pos + 1); + assert line1pos != -1; + int line2pos = index.indexOf('\n', line1pos + 1); + assert line2pos != -1; + int line3pos = index.indexOf('\n', line2pos + 1); + assert line3pos != -1; + String inlineTraceData = ""; + if (inlineTraceData64 != null) { + inlineTraceData = + "\n<script id=\"viewer-data\" type=\"text/plain\">" + inlineTraceData64 + "</script>"; + } + return index.substring(0, line0pos + 1) + + replacement + + inlineTraceData + + index.substring(line3pos); + } + + private static String replaceIndexWebComponents(String index, String replacement) { + int start = index.indexOf("IO_PERFMARK_WEBCOMPONENTS"); + if (start == -1) { + throw new IllegalArgumentException("index doesn't contain IO_PERFMARK_WEBCOMPONENTS"); + } + int line0pos = index.lastIndexOf('\n', start); + assert line0pos != -1; + int line1pos = index.indexOf('\n', line0pos + 1); + assert line1pos != -1; + + return index.substring(0, line0pos + 1) + replacement + index.substring(line1pos); + } + + private static String replaceIndexTraceData(String index, String replacement) { + int start = index.indexOf("IO_PERFMARK_TRACE_URL"); + if (start == -1) { + throw new IllegalArgumentException("index doesn't contain IO_PERFMARK_TRACE_URL"); + } + int line0pos = index.lastIndexOf('\n', start); + assert line0pos != -1; + int line1pos = index.indexOf('\n', line0pos + 1); + assert line1pos != -1; + int line2pos = index.indexOf('\n', line1pos + 1); + assert line2pos != -1; + int line3pos = index.indexOf('\n', line2pos + 1); + assert line3pos != -1; + return index.substring(0, line0pos + 1) + replacement + index.substring(line3pos); + } + + private static String trimTraceViewer(String traceViewer) { + int startpos = traceViewer.indexOf("<template"); + int lastpos = traceViewer.lastIndexOf("</head>"); + return traceViewer.substring(startpos, lastpos); + } + + private static String readAll(InputStream stream) throws IOException { + int available = stream.available(); + byte[] data; + if (available > 0) { + data = new byte[available + 1]; + } else { + data = new byte[4096]; + } + int pos = 0; + while (true) { + int read = stream.read(data, pos, data.length - pos); + if (read == -1) { + break; + } else { + pos += read; + } + if (pos == data.length) { + data = Arrays.copyOf(data, data.length + (data.length >> 2)); + } + } + return new String(data, 0, pos, UTF_8); + } + + private TraceEventViewer() { + throw new AssertionError("nope"); + } +} diff --git a/traceviewer/src/main/java/io/perfmark/traceviewer/package-info.java b/traceviewer/src/main/java/io/perfmark/traceviewer/package-info.java new file mode 100644 index 0000000..c9449e9 --- /dev/null +++ b/traceviewer/src/main/java/io/perfmark/traceviewer/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +/** + * The Trace View package reads the PerfMark recorded tasks, and converts them into the + * Chrome Trace Viewer format, bundled into a self-contained HTML page. + */ +@javax.annotation.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +package io.perfmark.traceviewer; diff --git a/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/LICENSE b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/LICENSE new file mode 100644 index 0000000..c992fe4 --- /dev/null +++ b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/LICENSE @@ -0,0 +1,27 @@ +Copyright 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of catapult nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/index.html b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/index.html new file mode 100644 index 0000000..e0abd7e --- /dev/null +++ b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/index.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<head> + <meta charset="utf-8"> + <script> + // IO_PERFMARK_WEBCOMPONENTS + </script> + <script> +'use strict'; + +function onTraceViewerImportFail() { + document.addEventListener('DOMContentLoaded', function() { + document.body.textContent = + 'tracing/bin/trace_viewer_full.html is missing. ' + + 'Run vulcanize_trace_viewer from $TRACE_VIEWER and reload.'; + }); +} +</script> +<!-- IO_PERFMARK_TRACE_IMPORT --> + <link rel="import" href="trace_viewer_full.html" + onerror="onTraceViewerImportFail(event)"> + + <style> + html, body { + box-sizing: border-box; + overflow: hidden; + margin: 0px; + padding: 0; + width: 100%; + height: 100%; + } + #trace-viewer { + width: 100%; + height: 100%; + } + #trace-viewer:focus { + outline: none; + } +</style> + <script> +'use strict'; + +(function() { + let viewer; + let url; + let model; + + // You can set this to true if you want to hide the WebComponentsV0 polyfill + // warning. + window.__hideTraceViewerPolyfillWarning = false; + + function load() { + const req = new XMLHttpRequest(); + const isBinary = /[.]gz$/.test(url) || /[.]zip$/.test(url); + req.overrideMimeType('text/plain; charset=x-user-defined'); + req.open('GET', url, true); + if (isBinary) req.responseType = 'arraybuffer'; + + req.onreadystatechange = function(event) { + if (req.readyState !== 4) return; + + window.setTimeout(function() { + if (req.status === 200) { + onResult(isBinary ? req.response : req.responseText); + } else { + onResultFail(req.status); + } + }, 0); + }; + req.send(null); + } + + function onResultFail(err) { + const overlay = new tr.ui.b.Overlay(); + overlay.textContent = err + ': ' + url + ' could not be loaded'; + overlay.title = 'Failed to fetch data'; + overlay.visible = true; + } + + function onResult(result) { + model = new tr.Model(); + const i = new tr.importer.Import(model); + const p = i.importTracesWithProgressDialog([result]); + p.then(onModelLoaded, onImportFail); + } + + function onModelLoaded() { + viewer.model = model; + viewer.viewTitle = url; + } + + function onImportFail() { + const overlay = new tr.ui.b.Overlay(); + overlay.textContent = tr.b.normalizeException(err).message; + overlay.title = 'Import error'; + overlay.visible = true; + } + + document.addEventListener('WebComponentsReady', function() { + const container = document.createElement('track-view-container'); + container.id = 'track_view_container'; + + viewer = document.createElement('tr-ui-timeline-view'); + viewer.track_view_container = container; + Polymer.dom(viewer).appendChild(container); + + viewer.id = 'trace-viewer'; + viewer.globalMode = true; + Polymer.dom(document.body).appendChild(viewer); + + // IO_PERFMARK_TRACE_URL + url = './trace.json'; + load(); + }); +}()); +</script> +</head> +<body> +</body> +</html> diff --git a/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/trace_viewer_full.html b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/trace_viewer_full.html new file mode 100644 index 0000000..30b70db --- /dev/null +++ b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/trace_viewer_full.html @@ -0,0 +1 @@ +file traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/catapult/trace_viewer_full.html is too large for review but will still be cloned after the change is approved
\ No newline at end of file diff --git a/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/polymer/LICENSE b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/polymer/LICENSE new file mode 100644 index 0000000..92d60b0 --- /dev/null +++ b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/polymer/LICENSE @@ -0,0 +1,27 @@ +// Copyright (c) 2012 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/polymer/webcomponents.min.js b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/polymer/webcomponents.min.js new file mode 100644 index 0000000..ad8196b --- /dev/null +++ b/traceviewer/src/main/resources/io/perfmark/traceviewer/third_party/polymer/webcomponents.min.js @@ -0,0 +1,14 @@ +/** + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +// @version 0.7.24 +!function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,r=e.split("=");r[0]&&(t=r[0].match(/wc-(.+)/))&&(n[t[1]]=r[1]||!0)}),t)for(var r,o=0;r=t.attributes[o];o++)"src"!==r.name&&(n[r.name]=r.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.shadow=n.shadow||n.shadowdom||n.polyfill,"native"===n.shadow?n.shadow=!1:n.shadow=n.shadow||!HTMLElement.prototype.createShadowRoot,n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),WebComponents.flags.shadow&&("undefined"==typeof WeakMap&&!function(){var e=Object.defineProperty,t=Date.now()%1e9,n=function(){this.name="__st"+(1e9*Math.random()>>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return!(!t||t[0]!==e)&&(t[0]=t[1]=void 0,!0)},has:function(e){var t=e[this.name];return!!t&&t[0]===e}},window.WeakMap=n}(),window.ShadowDOMPolyfill={},function(e){"use strict";function t(){if("undefined"!=typeof chrome&&chrome.app&&chrome.app.runtime)return!1;if(navigator.getDeviceStorage)return!1;try{var e=new Function("return true;");return e()}catch(t){return!1}}function n(e){if(!e)throw new Error("Assertion failed")}function r(e,t){for(var n=W(t),r=0;r<n.length;r++){var o=n[r];A(e,o,F(t,o))}return e}function o(e,t){for(var n=W(t),r=0;r<n.length;r++){var o=n[r];switch(o){case"arguments":case"caller":case"length":case"name":case"prototype":case"toString":continue}A(e,o,F(t,o))}return e}function i(e,t){for(var n=0;n<t.length;n++)if(t[n]in e)return t[n]}function a(e,t,n){U.value=n,A(e,t,U)}function s(e,t){var n=e.__proto__||Object.getPrototypeOf(e);if(q)try{W(n)}catch(r){n=n.__proto__}var o=R.get(n);if(o)return o;var i=s(n),a=E(i);return g(n,a,t),a}function c(e,t){w(e,t,!0)}function l(e,t){w(t,e,!1)}function u(e){return/^on[a-z]+$/.test(e)}function d(e){return/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(e)}function p(e){return k&&d(e)?new Function("return this.__impl4cf1e782hg__."+e):function(){return this.__impl4cf1e782hg__[e]}}function h(e){return k&&d(e)?new Function("v","this.__impl4cf1e782hg__."+e+" = v"):function(t){this.__impl4cf1e782hg__[e]=t}}function f(e){return k&&d(e)?new Function("return this.__impl4cf1e782hg__."+e+".apply(this.__impl4cf1e782hg__, arguments)"):function(){return this.__impl4cf1e782hg__[e].apply(this.__impl4cf1e782hg__,arguments)}}function m(e,t){try{return e===window&&"showModalDialog"===t?B:Object.getOwnPropertyDescriptor(e,t)}catch(n){return B}}function w(t,n,r,o){for(var i=W(t),a=0;a<i.length;a++){var s=i[a];if("polymerBlackList_"!==s&&!(s in n||t.polymerBlackList_&&t.polymerBlackList_[s])){q&&t.__lookupGetter__(s);var c,l,d=m(t,s);if("function"!=typeof d.value){var w=u(s);c=w?e.getEventHandlerGetter(s):p(s),(d.writable||d.set||V)&&(l=w?e.getEventHandlerSetter(s):h(s));var v=V||d.configurable;A(n,s,{get:c,set:l,configurable:v,enumerable:d.enumerable})}else r&&(n[s]=f(s))}}}function v(e,t,n){if(null!=e){var r=e.prototype;g(r,t,n),o(t,e)}}function g(e,t,r){var o=t.prototype;n(void 0===R.get(e)),R.set(e,t),I.set(o,e),c(e,o),r&&l(o,r),a(o,"constructor",t),t.prototype=o}function b(e,t){return R.get(t.prototype)===e}function y(e){var t=Object.getPrototypeOf(e),n=s(t),r=E(n);return g(t,r,e),r}function E(e){function t(t){e.call(this,t)}var n=Object.create(e.prototype);return n.constructor=t,t.prototype=n,t}function _(e){return e&&e.__impl4cf1e782hg__}function S(e){return!_(e)}function T(e){if(null===e)return null;n(S(e));var t=e.__wrapper8e3dd93a60__;return null!=t?t:e.__wrapper8e3dd93a60__=new(s(e,e))(e)}function M(e){return null===e?null:(n(_(e)),e.__impl4cf1e782hg__)}function O(e){return e.__impl4cf1e782hg__}function L(e,t){t.__impl4cf1e782hg__=e,e.__wrapper8e3dd93a60__=t}function N(e){return e&&_(e)?M(e):e}function C(e){return e&&!_(e)?T(e):e}function j(e,t){null!==t&&(n(S(e)),n(void 0===t||_(t)),e.__wrapper8e3dd93a60__=t)}function D(e,t,n){G.get=n,A(e.prototype,t,G)}function H(e,t){D(e,t,function(){return T(this.__impl4cf1e782hg__[t])})}function x(e,t){e.forEach(function(e){t.forEach(function(t){e.prototype[t]=function(){var e=C(this);return e[t].apply(e,arguments)}})})}var R=new WeakMap,I=new WeakMap,P=Object.create(null),k=t(),A=Object.defineProperty,W=Object.getOwnPropertyNames,F=Object.getOwnPropertyDescriptor,U={value:void 0,configurable:!0,enumerable:!1,writable:!0};W(window);var q=/Firefox/.test(navigator.userAgent),B={get:function(){},set:function(e){},configurable:!0,enumerable:!0},V=function(){var e=Object.getOwnPropertyDescriptor(Node.prototype,"nodeType");return e&&!e.get&&!e.set}(),G={get:void 0,configurable:!0,enumerable:!0};e.addForwardingProperties=c,e.assert=n,e.constructorTable=R,e.defineGetter=D,e.defineWrapGetter=H,e.forwardMethodsToWrapper=x,e.isIdentifierName=d,e.isWrapper=_,e.isWrapperFor=b,e.mixin=r,e.nativePrototypeTable=I,e.oneOf=i,e.registerObject=y,e.registerWrapper=v,e.rewrap=j,e.setWrapper=L,e.unsafeUnwrap=O,e.unwrap=M,e.unwrapIfNeeded=N,e.wrap=T,e.wrapIfNeeded=C,e.wrappers=P}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e,t,n){return{index:e,removed:t,addedCount:n}}function n(){}var r=0,o=1,i=2,a=3;n.prototype={calcEditDistances:function(e,t,n,r,o,i){for(var a=i-o+1,s=n-t+1,c=new Array(a),l=0;l<a;l++)c[l]=new Array(s),c[l][0]=l;for(var u=0;u<s;u++)c[0][u]=u;for(var l=1;l<a;l++)for(var u=1;u<s;u++)if(this.equals(e[t+u-1],r[o+l-1]))c[l][u]=c[l-1][u-1];else{var d=c[l-1][u]+1,p=c[l][u-1]+1;c[l][u]=d<p?d:p}return c},spliceOperationsFromEditDistances:function(e){for(var t=e.length-1,n=e[0].length-1,s=e[t][n],c=[];t>0||n>0;)if(0!=t)if(0!=n){var l,u=e[t-1][n-1],d=e[t-1][n],p=e[t][n-1];l=d<p?d<u?d:u:p<u?p:u,l==u?(u==s?c.push(r):(c.push(o),s=u),t--,n--):l==d?(c.push(a),t--,s=d):(c.push(i),n--,s=p)}else c.push(a),t--;else c.push(i),n--;return c.reverse(),c},calcSplices:function(e,n,s,c,l,u){var d=0,p=0,h=Math.min(s-n,u-l);if(0==n&&0==l&&(d=this.sharedPrefix(e,c,h)),s==e.length&&u==c.length&&(p=this.sharedSuffix(e,c,h-d)),n+=d,l+=d,s-=p,u-=p,s-n==0&&u-l==0)return[];if(n==s){for(var f=t(n,[],0);l<u;)f.removed.push(c[l++]);return[f]}if(l==u)return[t(n,[],s-n)];for(var m=this.spliceOperationsFromEditDistances(this.calcEditDistances(e,n,s,c,l,u)),f=void 0,w=[],v=n,g=l,b=0;b<m.length;b++)switch(m[b]){case r:f&&(w.push(f),f=void 0),v++,g++;break;case o:f||(f=t(v,[],0)),f.addedCount++,v++,f.removed.push(c[g]),g++;break;case i:f||(f=t(v,[],0)),f.addedCount++,v++;break;case a:f||(f=t(v,[],0)),f.removed.push(c[g]),g++}return f&&w.push(f),w},sharedPrefix:function(e,t,n){for(var r=0;r<n;r++)if(!this.equals(e[r],t[r]))return r;return n},sharedSuffix:function(e,t,n){for(var r=e.length,o=t.length,i=0;i<n&&this.equals(e[--r],t[--o]);)i++;return i},calculateSplices:function(e,t){return this.calcSplices(e,0,e.length,t,0,t.length)},equals:function(e,t){return e===t}},e.ArraySplice=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(){a=!1;var e=i.slice(0);i=[];for(var t=0;t<e.length;t++)(0,e[t])()}function n(e){i.push(e),a||(a=!0,r(t,0))}var r,o=window.MutationObserver,i=[],a=!1;if(o){var s=1,c=new o(t),l=document.createTextNode(s);c.observe(l,{characterData:!0}),r=function(){s=(s+1)%2,l.data=s}}else r=window.setTimeout;e.setEndOfMicrotask=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){e.scheduled_||(e.scheduled_=!0,f.push(e),m||(u(n),m=!0))}function n(){for(m=!1;f.length;){var e=f;f=[],e.sort(function(e,t){return e.uid_-t.uid_});for(var t=0;t<e.length;t++){var n=e[t];n.scheduled_=!1;var r=n.takeRecords();i(n),r.length&&n.callback_(r,n)}}}function r(e,t){this.type=e,this.target=t,this.addedNodes=new p.NodeList,this.removedNodes=new p.NodeList,this.previousSibling=null,this.nextSibling=null,this.attributeName=null,this.attributeNamespace=null,this.oldValue=null}function o(e,t){for(;e;e=e.parentNode){var n=h.get(e);if(n)for(var r=0;r<n.length;r++){var o=n[r];o.options.subtree&&o.addTransientObserver(t)}}}function i(e){for(var t=0;t<e.nodes_.length;t++){var n=e.nodes_[t],r=h.get(n);if(!r)return;for(var o=0;o<r.length;o++){var i=r[o];i.observer===e&&i.removeTransientObservers()}}}function a(e,n,o){for(var i=Object.create(null),a=Object.create(null),s=e;s;s=s.parentNode){var c=h.get(s);if(c)for(var l=0;l<c.length;l++){var u=c[l],d=u.options;if((s===e||d.subtree)&&("attributes"!==n||d.attributes)&&("attributes"!==n||!d.attributeFilter||null===o.namespace&&d.attributeFilter.indexOf(o.name)!==-1)&&("characterData"!==n||d.characterData)&&("childList"!==n||d.childList)){var p=u.observer;i[p.uid_]=p,("attributes"===n&&d.attributeOldValue||"characterData"===n&&d.characterDataOldValue)&&(a[p.uid_]=o.oldValue)}}}for(var f in i){var p=i[f],m=new r(n,e);"name"in o&&"namespace"in o&&(m.attributeName=o.name,m.attributeNamespace=o.namespace),o.addedNodes&&(m.addedNodes=o.addedNodes),o.removedNodes&&(m.removedNodes=o.removedNodes),o.previousSibling&&(m.previousSibling=o.previousSibling),o.nextSibling&&(m.nextSibling=o.nextSibling),void 0!==a[f]&&(m.oldValue=a[f]),t(p),p.records_.push(m)}}function s(e){if(this.childList=!!e.childList,this.subtree=!!e.subtree,"attributes"in e||!("attributeOldValue"in e||"attributeFilter"in e)?this.attributes=!!e.attributes:this.attributes=!0,"characterDataOldValue"in e&&!("characterData"in e)?this.characterData=!0:this.characterData=!!e.characterData,!this.attributes&&(e.attributeOldValue||"attributeFilter"in e)||!this.characterData&&e.characterDataOldValue)throw new TypeError;if(this.characterData=!!e.characterData,this.attributeOldValue=!!e.attributeOldValue,this.characterDataOldValue=!!e.characterDataOldValue,"attributeFilter"in e){if(null==e.attributeFilter||"object"!=typeof e.attributeFilter)throw new TypeError;this.attributeFilter=w.call(e.attributeFilter)}else this.attributeFilter=null}function c(e){this.callback_=e,this.nodes_=[],this.records_=[],this.uid_=++v,this.scheduled_=!1}function l(e,t,n){this.observer=e,this.target=t,this.options=n,this.transientObservedNodes=[]}var u=e.setEndOfMicrotask,d=e.wrapIfNeeded,p=e.wrappers,h=new WeakMap,f=[],m=!1,w=Array.prototype.slice,v=0;c.prototype={constructor:c,observe:function(e,t){e=d(e);var n,r=new s(t),o=h.get(e);o||h.set(e,o=[]);for(var i=0;i<o.length;i++)o[i].observer===this&&(n=o[i],n.removeTransientObservers(),n.options=r);n||(n=new l(this,e,r),o.push(n),this.nodes_.push(e))},disconnect:function(){this.nodes_.forEach(function(e){for(var t=h.get(e),n=0;n<t.length;n++){var r=t[n];if(r.observer===this){t.splice(n,1);break}}},this),this.records_=[]},takeRecords:function(){var e=this.records_;return this.records_=[],e}},l.prototype={addTransientObserver:function(e){if(e!==this.target){t(this.observer),this.transientObservedNodes.push(e);var n=h.get(e);n||h.set(e,n=[]),n.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[];for(var t=0;t<e.length;t++)for(var n=e[t],r=h.get(n),o=0;o<r.length;o++)if(r[o]===this){r.splice(o,1);break}}},e.enqueueMutation=a,e.registerTransientObservers=o,e.wrappers.MutationObserver=c,e.wrappers.MutationRecord=r}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e,t){this.root=e,this.parent=t}function n(e,t){if(e.treeScope_!==t){e.treeScope_=t;for(var r=e.shadowRoot;r;r=r.olderShadowRoot)r.treeScope_.parent=t;for(var o=e.firstChild;o;o=o.nextSibling)n(o,t)}}function r(n){if(n instanceof e.wrappers.Window,n.treeScope_)return n.treeScope_;var o,i=n.parentNode;return o=i?r(i):new t(n,null),n.treeScope_=o}t.prototype={get renderer(){return this.root instanceof e.wrappers.ShadowRoot?e.getRendererForHost(this.root.host):null},contains:function(e){for(;e;e=e.parent)if(e===this)return!0;return!1}},e.TreeScope=t,e.getTreeScope=r,e.setTreeScope=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){return e instanceof G.ShadowRoot}function n(e){return A(e).root}function r(e,r){var s=[],c=e;for(s.push(c);c;){var l=a(c);if(l&&l.length>0){for(var u=0;u<l.length;u++){var p=l[u];if(i(p)){var h=n(p),f=h.olderShadowRoot;f&&s.push(f)}s.push(p)}c=l[l.length-1]}else if(t(c)){if(d(e,c)&&o(r))break;c=c.host,s.push(c)}else c=c.parentNode,c&&s.push(c)}return s}function o(e){if(!e)return!1;switch(e.type){case"abort":case"error":case"select":case"change":case"load":case"reset":case"resize":case"scroll":case"selectstart":return!0}return!1}function i(e){return e instanceof HTMLShadowElement}function a(t){return e.getDestinationInsertionPoints(t)}function s(e,t){if(0===e.length)return t;t instanceof G.Window&&(t=t.document);for(var n=A(t),r=e[0],o=A(r),i=l(n,o),a=0;a<e.length;a++){var s=e[a];if(A(s)===i)return s}return e[e.length-1]}function c(e){for(var t=[];e;e=e.parent)t.push(e);return t}function l(e,t){for(var n=c(e),r=c(t),o=null;n.length>0&&r.length>0;){var i=n.pop(),a=r.pop();if(i!==a)break;o=i}return o}function u(e,t,n){t instanceof G.Window&&(t=t.document);var o,i=A(t),a=A(n),s=r(n,e),o=l(i,a);o||(o=a.root);for(var c=o;c;c=c.parent)for(var u=0;u<s.length;u++){var d=s[u];if(A(d)===c)return d}return null}function d(e,t){return A(e)===A(t)}function p(e){if(!K.get(e)&&(K.set(e,!0),f(V(e),V(e.target)),P)){var t=P;throw P=null,t}}function h(e){switch(e.type){case"load":case"beforeunload":case"unload":return!0}return!1}function f(t,n){if($.get(t))throw new Error("InvalidStateError");$.set(t,!0),e.renderAllPending();var o,i,a;if(h(t)&&!t.bubbles){var s=n;s instanceof G.Document&&(a=s.defaultView)&&(i=s,o=[])}if(!o)if(n instanceof G.Window)a=n,o=[];else if(o=r(n,t),!h(t)){var s=o[o.length-1];s instanceof G.Document&&(a=s.defaultView)}return ne.set(t,o),m(t,o,a,i)&&w(t,o,a,i)&&v(t,o,a,i),J.set(t,re),Y["delete"](t,null),$["delete"](t),t.defaultPrevented}function m(e,t,n,r){var o=oe;if(n&&!g(n,e,o,t,r))return!1;for(var i=t.length-1;i>0;i--)if(!g(t[i],e,o,t,r))return!1;return!0}function w(e,t,n,r){var o=ie,i=t[0]||n;return g(i,e,o,t,r)}function v(e,t,n,r){for(var o=ae,i=1;i<t.length;i++)if(!g(t[i],e,o,t,r))return;n&&t.length>0&&g(n,e,o,t,r)}function g(e,t,n,r,o){var i=z.get(e);if(!i)return!0;var a=o||s(r,e);if(a===e){if(n===oe)return!0;n===ae&&(n=ie)}else if(n===ae&&!t.bubbles)return!0;if("relatedTarget"in t){var c=B(t),l=c.relatedTarget;if(l){if(l instanceof Object&&l.addEventListener){var d=V(l),p=u(t,e,d);if(p===a)return!0}else p=null;Z.set(t,p)}}J.set(t,n);var h=t.type,f=!1;X.set(t,a),Y.set(t,e),i.depth++;for(var m=0,w=i.length;m<w;m++){var v=i[m];if(v.removed)f=!0;else if(!(v.type!==h||!v.capture&&n===oe||v.capture&&n===ae))try{if("function"==typeof v.handler?v.handler.call(e,t):v.handler.handleEvent(t),ee.get(t))return!1}catch(g){P||(P=g)}}if(i.depth--,f&&0===i.depth){var b=i.slice();i.length=0;for(var m=0;m<b.length;m++)b[m].removed||i.push(b[m])}return!Q.get(t)}function b(e,t,n){this.type=e,this.handler=t,this.capture=Boolean(n)}function y(e,t){if(!(e instanceof se))return V(T(se,"Event",e,t));var n=e;return be||"beforeunload"!==n.type||this instanceof M?void U(n,this):new M(n)}function E(e){return e&&e.relatedTarget?Object.create(e,{relatedTarget:{value:B(e.relatedTarget)}}):e}function _(e,t,n){var r=window[e],o=function(t,n){return t instanceof r?void U(t,this):V(T(r,e,t,n))};if(o.prototype=Object.create(t.prototype),n&&W(o.prototype,n),r)try{F(r,o,new r("temp"))}catch(i){F(r,o,document.createEvent(e))}return o}function S(e,t){return function(){arguments[t]=B(arguments[t]);var n=B(this);n[e].apply(n,arguments)}}function T(e,t,n,r){if(ve)return new e(n,E(r));var o=B(document.createEvent(t)),i=we[t],a=[n];return Object.keys(i).forEach(function(e){var t=null!=r&&e in r?r[e]:i[e];"relatedTarget"===e&&(t=B(t)),a.push(t)}),o["init"+t].apply(o,a),o}function M(e){y.call(this,e)}function O(e){return"function"==typeof e||e&&e.handleEvent}function L(e){switch(e){case"DOMAttrModified":case"DOMAttributeNameChanged":case"DOMCharacterDataModified":case"DOMElementNameChanged":case"DOMNodeInserted":case"DOMNodeInsertedIntoDocument":case"DOMNodeRemoved":case"DOMNodeRemovedFromDocument":case"DOMSubtreeModified":return!0}return!1}function N(e){U(e,this)}function C(e){return e instanceof G.ShadowRoot&&(e=e.host),B(e)}function j(e,t){var n=z.get(e);if(n)for(var r=0;r<n.length;r++)if(!n[r].removed&&n[r].type===t)return!0;return!1}function D(e,t){for(var n=B(e);n;n=n.parentNode)if(j(V(n),t))return!0;return!1}function H(e){k(e,Ee)}function x(t,n,o,i){e.renderAllPending();var a=V(_e.call(q(n),o,i));if(!a)return null;var c=r(a,null),l=c.lastIndexOf(t);return l==-1?null:(c=c.slice(0,l),s(c,t))}function R(e){return function(){var t=te.get(this);return t&&t[e]&&t[e].value||null}}function I(e){var t=e.slice(2);return function(n){var r=te.get(this);r||(r=Object.create(null),te.set(this,r));var o=r[e];if(o&&this.removeEventListener(t,o.wrapped,!1),"function"==typeof n){var i=function(t){var r=n.call(this,t);r===!1?t.preventDefault():"onbeforeunload"===e&&"string"==typeof r&&(t.returnValue=r)};this.addEventListener(t,i,!1),r[e]={value:n,wrapped:i}}}}var P,k=e.forwardMethodsToWrapper,A=e.getTreeScope,W=e.mixin,F=e.registerWrapper,U=e.setWrapper,q=e.unsafeUnwrap,B=e.unwrap,V=e.wrap,G=e.wrappers,z=(new WeakMap,new WeakMap),K=new WeakMap,$=new WeakMap,X=new WeakMap,Y=new WeakMap,Z=new WeakMap,J=new WeakMap,Q=new WeakMap,ee=new WeakMap,te=new WeakMap,ne=new WeakMap,re=0,oe=1,ie=2,ae=3;b.prototype={equals:function(e){return this.handler===e.handler&&this.type===e.type&&this.capture===e.capture},get removed(){return null===this.handler},remove:function(){this.handler=null}};var se=window.Event;se.prototype.polymerBlackList_={returnValue:!0,keyLocation:!0},y.prototype={get target(){return X.get(this)},get currentTarget(){return Y.get(this)},get eventPhase(){return J.get(this)},get path(){var e=ne.get(this);return e?e.slice():[]},stopPropagation:function(){Q.set(this,!0)},stopImmediatePropagation:function(){Q.set(this,!0),ee.set(this,!0)}};var ce=function(){var e=document.createEvent("Event");return e.initEvent("test",!0,!0),e.preventDefault(),e.defaultPrevented}();ce||(y.prototype.preventDefault=function(){this.cancelable&&(q(this).preventDefault(),Object.defineProperty(this,"defaultPrevented",{get:function(){return!0},configurable:!0}))}),F(se,y,document.createEvent("Event"));var le=_("UIEvent",y),ue=_("CustomEvent",y),de={get relatedTarget(){var e=Z.get(this);return void 0!==e?e:V(B(this).relatedTarget)}},pe=W({initMouseEvent:S("initMouseEvent",14)},de),he=W({initFocusEvent:S("initFocusEvent",5)},de),fe=_("MouseEvent",le,pe),me=_("FocusEvent",le,he),we=Object.create(null),ve=function(){try{new window.FocusEvent("focus")}catch(e){return!1}return!0}();if(!ve){var ge=function(e,t,n){if(n){var r=we[n];t=W(W({},r),t)}we[e]=t};ge("Event",{bubbles:!1,cancelable:!1}),ge("CustomEvent",{detail:null},"Event"),ge("UIEvent",{view:null,detail:0},"Event"),ge("MouseEvent",{screenX:0,screenY:0,clientX:0,clientY:0,ctrlKey:!1,altKey:!1,shiftKey:!1,metaKey:!1,button:0,relatedTarget:null},"UIEvent"),ge("FocusEvent",{relatedTarget:null},"UIEvent")}var be=window.BeforeUnloadEvent;M.prototype=Object.create(y.prototype),W(M.prototype,{get returnValue(){return q(this).returnValue},set returnValue(e){q(this).returnValue=e}}),be&&F(be,M);var ye=window.EventTarget,Ee=["addEventListener","removeEventListener","dispatchEvent"];[Node,Window].forEach(function(e){var t=e.prototype;Ee.forEach(function(e){Object.defineProperty(t,e+"_",{value:t[e]})})}),N.prototype={addEventListener:function(e,t,n){if(O(t)&&!L(e)){var r=new b(e,t,n),o=z.get(this);if(o){for(var i=0;i<o.length;i++)if(r.equals(o[i]))return}else o=[],o.depth=0,z.set(this,o);o.push(r);var a=C(this);a.addEventListener_(e,p,!0)}},removeEventListener:function(e,t,n){n=Boolean(n);var r=z.get(this);if(r){for(var o=0,i=!1,a=0;a<r.length;a++)r[a].type===e&&r[a].capture===n&&(o++,r[a].handler===t&&(i=!0,r[a].remove()));if(i&&1===o){var s=C(this);s.removeEventListener_(e,p,!0)}}},dispatchEvent:function(t){var n=B(t),r=n.type;K.set(n,!1),e.renderAllPending();var o;D(this,r)||(o=function(){},this.addEventListener(r,o,!0));try{return B(this).dispatchEvent_(n)}finally{o&&this.removeEventListener(r,o,!0)}}},ye&&F(ye,N);var _e=document.elementFromPoint;e.elementFromPoint=x,e.getEventHandlerGetter=R,e.getEventHandlerSetter=I,e.wrapEventTargetMethods=H,e.wrappers.BeforeUnloadEvent=M,e.wrappers.CustomEvent=ue,e.wrappers.Event=y,e.wrappers.EventTarget=N,e.wrappers.FocusEvent=me,e.wrappers.MouseEvent=fe,e.wrappers.UIEvent=le}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e,t){Object.defineProperty(e,t,m)}function n(e){l(e,this)}function r(){this.length=0,t(this,"length")}function o(e){for(var t=new r,o=0;o<e.length;o++)t[o]=new n(e[o]);return t.length=o,t}function i(e){a.call(this,e)}var a=e.wrappers.UIEvent,s=e.mixin,c=e.registerWrapper,l=e.setWrapper,u=e.unsafeUnwrap,d=e.wrap,p=window.TouchEvent;if(p){var h;try{h=document.createEvent("TouchEvent")}catch(f){return}var m={enumerable:!1};n.prototype={get target(){return d(u(this).target)}};var w={configurable:!0,enumerable:!0,get:null};["clientX","clientY","screenX","screenY","pageX","pageY","identifier","webkitRadiusX","webkitRadiusY","webkitRotationAngle","webkitForce"].forEach(function(e){w.get=function(){return u(this)[e]},Object.defineProperty(n.prototype,e,w)}),r.prototype={item:function(e){return this[e]}},i.prototype=Object.create(a.prototype),s(i.prototype,{get touches(){return o(u(this).touches)},get targetTouches(){return o(u(this).targetTouches)},get changedTouches(){return o(u(this).changedTouches)},initTouchEvent:function(){throw new Error("Not implemented")}}),c(p,i,h),e.wrappers.Touch=n,e.wrappers.TouchEvent=i,e.wrappers.TouchList=r}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e,t){Object.defineProperty(e,t,s)}function n(){this.length=0,t(this,"length")}function r(e){if(null==e)return e;for(var t=new n,r=0,o=e.length;r<o;r++)t[r]=a(e[r]);return t.length=o,t}function o(e,t){e.prototype[t]=function(){return r(i(this)[t].apply(i(this),arguments))}}var i=e.unsafeUnwrap,a=e.wrap,s={enumerable:!1};n.prototype={item:function(e){return this[e]}},t(n.prototype,"item"),e.wrappers.NodeList=n,e.addWrapNodeListMethod=o,e.wrapNodeList=r}(window.ShadowDOMPolyfill),function(e){"use strict";e.wrapHTMLCollection=e.wrapNodeList,e.wrappers.HTMLCollection=e.wrappers.NodeList}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){O(e instanceof _)}function n(e){var t=new T;return t[0]=e,t.length=1,t}function r(e,t,n){N(t,"childList",{removedNodes:n,previousSibling:e.previousSibling,nextSibling:e.nextSibling})}function o(e,t){N(e,"childList",{removedNodes:t})}function i(e,t,r,o){if(e instanceof DocumentFragment){var i=s(e);U=!0;for(var a=i.length-1;a>=0;a--)e.removeChild(i[a]),i[a].parentNode_=t;U=!1;for(var a=0;a<i.length;a++)i[a].previousSibling_=i[a-1]||r,i[a].nextSibling_=i[a+1]||o;return r&&(r.nextSibling_=i[0]),o&&(o.previousSibling_=i[i.length-1]),i}var i=n(e),c=e.parentNode;return c&&c.removeChild(e),e.parentNode_=t,e.previousSibling_=r,e.nextSibling_=o,r&&(r.nextSibling_=e),o&&(o.previousSibling_=e),i}function a(e){if(e instanceof DocumentFragment)return s(e);var t=n(e),o=e.parentNode;return o&&r(e,o,t),t}function s(e){for(var t=new T,n=0,r=e.firstChild;r;r=r.nextSibling)t[n++]=r;return t.length=n,o(e,t),t}function c(e){return e}function l(e,t){R(e,t),e.nodeIsInserted_()}function u(e,t){for(var n=C(t),r=0;r<e.length;r++)l(e[r],n)}function d(e){R(e,new M(e,null))}function p(e){for(var t=0;t<e.length;t++)d(e[t])}function h(e,t){var n=e.nodeType===_.DOCUMENT_NODE?e:e.ownerDocument;n!==t.ownerDocument&&n.adoptNode(t)}function f(t,n){if(n.length){var r=t.ownerDocument;if(r!==n[0].ownerDocument)for(var o=0;o<n.length;o++)e.adoptNodeNoRemove(n[o],r)}}function m(e,t){f(e,t);var n=t.length;if(1===n)return P(t[0]);for(var r=P(e.ownerDocument.createDocumentFragment()),o=0;o<n;o++)r.appendChild(P(t[o]));return r}function w(e){if(void 0!==e.firstChild_)for(var t=e.firstChild_;t;){var n=t;t=t.nextSibling_,n.parentNode_=n.previousSibling_=n.nextSibling_=void 0}e.firstChild_=e.lastChild_=void 0}function v(e){if(e.invalidateShadowRenderer()){for(var t=e.firstChild;t;){O(t.parentNode===e);var n=t.nextSibling,r=P(t),o=r.parentNode;o&&X.call(o,r),t.previousSibling_=t.nextSibling_=t.parentNode_=null,t=n}e.firstChild_=e.lastChild_=null}else for(var n,i=P(e),a=i.firstChild;a;)n=a.nextSibling,X.call(i,a),a=n}function g(e){var t=e.parentNode;return t&&t.invalidateShadowRenderer()}function b(e){for(var t,n=0;n<e.length;n++)t=e[n],t.parentNode.removeChild(t)}function y(e,t,n){var r;if(r=A(n?q.call(n,I(e),!1):B.call(I(e),!1)),t){for(var o=e.firstChild;o;o=o.nextSibling)r.appendChild(y(o,!0,n));if(e instanceof F.HTMLTemplateElement)for(var i=r.content,o=e.content.firstChild;o;o=o.nextSibling)i.appendChild(y(o,!0,n))}return r}function E(e,t){if(!t||C(e)!==C(t))return!1;for(var n=t;n;n=n.parentNode)if(n===e)return!0;return!1}function _(e){O(e instanceof V),S.call(this,e),this.parentNode_=void 0,this.firstChild_=void 0,this.lastChild_=void 0,this.nextSibling_=void 0,this.previousSibling_=void 0,this.treeScope_=void 0}var S=e.wrappers.EventTarget,T=e.wrappers.NodeList,M=e.TreeScope,O=e.assert,L=e.defineWrapGetter,N=e.enqueueMutation,C=e.getTreeScope,j=e.isWrapper,D=e.mixin,H=e.registerTransientObservers,x=e.registerWrapper,R=e.setTreeScope,I=e.unsafeUnwrap,P=e.unwrap,k=e.unwrapIfNeeded,A=e.wrap,W=e.wrapIfNeeded,F=e.wrappers,U=!1,q=document.importNode,B=window.Node.prototype.cloneNode,V=window.Node,G=window.DocumentFragment,z=(V.prototype.appendChild,V.prototype.compareDocumentPosition),K=V.prototype.isEqualNode,$=V.prototype.insertBefore,X=V.prototype.removeChild,Y=V.prototype.replaceChild,Z=/Trident|Edge/.test(navigator.userAgent),J=Z?function(e,t){try{X.call(e,t)}catch(n){if(!(e instanceof G))throw n}}:function(e,t){X.call(e,t)};_.prototype=Object.create(S.prototype),D(_.prototype,{appendChild:function(e){return this.insertBefore(e,null)},insertBefore:function(e,n){t(e);var r;n?j(n)?r=P(n):(r=n,n=A(r)):(n=null,r=null),n&&O(n.parentNode===this);var o,s=n?n.previousSibling:this.lastChild,c=!this.invalidateShadowRenderer()&&!g(e);if(o=c?a(e):i(e,this,s,n),c)h(this,e),w(this),$.call(I(this),P(e),r);else{s||(this.firstChild_=o[0]),n||(this.lastChild_=o[o.length-1],void 0===this.firstChild_&&(this.firstChild_=this.firstChild));var l=r?r.parentNode:I(this);l?$.call(l,m(this,o),r):f(this,o)}return N(this,"childList",{addedNodes:o,nextSibling:n,previousSibling:s}),u(o,this),e},removeChild:function(e){if(t(e),e.parentNode!==this){for(var r=!1,o=(this.childNodes,this.firstChild);o;o=o.nextSibling)if(o===e){r=!0;break}if(!r)throw new Error("NotFoundError")}var i=P(e),a=e.nextSibling,s=e.previousSibling;if(this.invalidateShadowRenderer()){var c=this.firstChild,l=this.lastChild,u=i.parentNode;u&&J(u,i),c===e&&(this.firstChild_=a),l===e&&(this.lastChild_=s),s&&(s.nextSibling_=a),a&&(a.previousSibling_=s),e.previousSibling_=e.nextSibling_=e.parentNode_=void 0}else w(this),J(I(this),i);return U||N(this,"childList",{removedNodes:n(e),nextSibling:a,previousSibling:s}),H(this,e),e},replaceChild:function(e,r){t(e);var o;if(j(r)?o=P(r):(o=r,r=A(o)),r.parentNode!==this)throw new Error("NotFoundError");var s,c=r.nextSibling,l=r.previousSibling,p=!this.invalidateShadowRenderer()&&!g(e);return p?s=a(e):(c===e&&(c=e.nextSibling),s=i(e,this,l,c)),p?(h(this,e),w(this),Y.call(I(this),P(e),o)):(this.firstChild===r&&(this.firstChild_=s[0]),this.lastChild===r&&(this.lastChild_=s[s.length-1]),r.previousSibling_=r.nextSibling_=r.parentNode_=void 0,o.parentNode&&Y.call(o.parentNode,m(this,s),o)),N(this,"childList",{addedNodes:s,removedNodes:n(r),nextSibling:c,previousSibling:l}),d(r),u(s,this),r},nodeIsInserted_:function(){for(var e=this.firstChild;e;e=e.nextSibling)e.nodeIsInserted_()},hasChildNodes:function(){return null!==this.firstChild},get parentNode(){return void 0!==this.parentNode_?this.parentNode_:A(I(this).parentNode)},get firstChild(){return void 0!==this.firstChild_?this.firstChild_:A(I(this).firstChild)},get lastChild(){return void 0!==this.lastChild_?this.lastChild_:A(I(this).lastChild)},get nextSibling(){return void 0!==this.nextSibling_?this.nextSibling_:A(I(this).nextSibling)},get previousSibling(){return void 0!==this.previousSibling_?this.previousSibling_:A(I(this).previousSibling)},get parentElement(){for(var e=this.parentNode;e&&e.nodeType!==_.ELEMENT_NODE;)e=e.parentNode;return e},get textContent(){for(var e="",t=this.firstChild;t;t=t.nextSibling)t.nodeType!=_.COMMENT_NODE&&(e+=t.textContent);return e},set textContent(e){null==e&&(e="");var t=c(this.childNodes);if(this.invalidateShadowRenderer()){if(v(this),""!==e){var n=I(this).ownerDocument.createTextNode(e);this.appendChild(n)}}else w(this),I(this).textContent=e;var r=c(this.childNodes);N(this,"childList",{addedNodes:r,removedNodes:t}),p(t),u(r,this)},get childNodes(){for(var e=new T,t=0,n=this.firstChild;n;n=n.nextSibling)e[t++]=n;return e.length=t,e},cloneNode:function(e){return y(this,e)},contains:function(e){return E(this,W(e))},compareDocumentPosition:function(e){return z.call(I(this),k(e))},isEqualNode:function(e){return K.call(I(this),k(e))},normalize:function(){for(var e,t,n=c(this.childNodes),r=[],o="",i=0;i<n.length;i++)t=n[i],t.nodeType===_.TEXT_NODE?e||t.data.length?e?(o+=t.data,r.push(t)):e=t:this.removeChild(t):(e&&r.length&&(e.data+=o,b(r)),r=[],o="",e=null,t.childNodes.length&&t.normalize());e&&r.length&&(e.data+=o,b(r))}}),L(_,"ownerDocument"),x(V,_,document.createDocumentFragment()),delete _.prototype.querySelector,delete _.prototype.querySelectorAll,_.prototype=D(Object.create(S.prototype),_.prototype),e.cloneNode=y,e.nodeWasAdded=l,e.nodeWasRemoved=d,e.nodesWereAdded=u,e.nodesWereRemoved=p,e.originalInsertBefore=$,e.originalRemoveChild=X,e.snapshotNodeList=c,e.wrappers.Node=_}(window.ShadowDOMPolyfill),function(e){"use strict";function t(t,n,r,o){for(var i=null,a=null,s=0,c=t.length;s<c;s++)i=b(t[s]),!o&&(a=v(i).root)&&a instanceof e.wrappers.ShadowRoot||(r[n++]=i);return n}function n(e){return String(e).replace(/\/deep\/|::shadow|>>>/g," ")}function r(e){return String(e).replace(/:host\(([^\s]+)\)/g,"$1").replace(/([^\s]):host/g,"$1").replace(":host","*").replace(/\^|\/shadow\/|\/shadow-deep\/|::shadow|\/deep\/|::content|>>>/g," ")}function o(e,t){for(var n,r=e.firstElementChild;r;){if(r.matches(t))return r;if(n=o(r,t))return n;r=r.nextElementSibling}return null}function i(e,t){return e.matches(t)}function a(e,t,n){var r=e.localName;return r===t||r===n&&e.namespaceURI===j}function s(){return!0}function c(e,t,n){return e.localName===n}function l(e,t){return e.namespaceURI===t}function u(e,t,n){return e.namespaceURI===t&&e.localName===n}function d(e,t,n,r,o,i){for(var a=e.firstElementChild;a;)r(a,o,i)&&(n[t++]=a),t=d(a,t,n,r,o,i),a=a.nextElementSibling;return t}function p(n,r,o,i,a){var s,c=g(this),l=v(this).root;if(l instanceof e.wrappers.ShadowRoot)return d(this,r,o,n,i,null);if(c instanceof N)s=S.call(c,i);else{if(!(c instanceof C))return d(this,r,o,n,i,null);s=_.call(c,i)}return t(s,r,o,a)}function h(n,r,o,i,a){var s,c=g(this),l=v(this).root;if(l instanceof e.wrappers.ShadowRoot)return d(this,r,o,n,i,a);if(c instanceof N)s=M.call(c,i,a);else{if(!(c instanceof C))return d(this,r,o,n,i,a);s=T.call(c,i,a)}return t(s,r,o,!1)}function f(n,r,o,i,a){var s,c=g(this),l=v(this).root;if(l instanceof e.wrappers.ShadowRoot)return d(this,r,o,n,i,a);if(c instanceof N)s=L.call(c,i,a);else{if(!(c instanceof C))return d(this,r,o,n,i,a);s=O.call(c,i,a)}return t(s,r,o,!1)}var m=e.wrappers.HTMLCollection,w=e.wrappers.NodeList,v=e.getTreeScope,g=e.unsafeUnwrap,b=e.wrap,y=document.querySelector,E=document.documentElement.querySelector,_=document.querySelectorAll,S=document.documentElement.querySelectorAll,T=document.getElementsByTagName,M=document.documentElement.getElementsByTagName,O=document.getElementsByTagNameNS,L=document.documentElement.getElementsByTagNameNS,N=window.Element,C=window.HTMLDocument||window.Document,j="http://www.w3.org/1999/xhtml",D={ +querySelector:function(t){var r=n(t),i=r!==t;t=r;var a,s=g(this),c=v(this).root;if(c instanceof e.wrappers.ShadowRoot)return o(this,t);if(s instanceof N)a=b(E.call(s,t));else{if(!(s instanceof C))return o(this,t);a=b(y.call(s,t))}return a&&!i&&(c=v(a).root)&&c instanceof e.wrappers.ShadowRoot?o(this,t):a},querySelectorAll:function(e){var t=n(e),r=t!==e;e=t;var o=new w;return o.length=p.call(this,i,0,o,e,r),o}},H={matches:function(t){return t=r(t),e.originalMatches.call(g(this),t)}},x={getElementsByTagName:function(e){var t=new m,n="*"===e?s:a;return t.length=h.call(this,n,0,t,e,e.toLowerCase()),t},getElementsByClassName:function(e){return this.querySelectorAll("."+e)},getElementsByTagNameNS:function(e,t){var n=new m,r=null;return r="*"===e?"*"===t?s:c:"*"===t?l:u,n.length=f.call(this,r,0,n,e||null,t),n}};e.GetElementsByInterface=x,e.SelectorsInterface=D,e.MatchesInterface=H}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){for(;e&&e.nodeType!==Node.ELEMENT_NODE;)e=e.nextSibling;return e}function n(e){for(;e&&e.nodeType!==Node.ELEMENT_NODE;)e=e.previousSibling;return e}var r=e.wrappers.NodeList,o={get firstElementChild(){return t(this.firstChild)},get lastElementChild(){return n(this.lastChild)},get childElementCount(){for(var e=0,t=this.firstElementChild;t;t=t.nextElementSibling)e++;return e},get children(){for(var e=new r,t=0,n=this.firstElementChild;n;n=n.nextElementSibling)e[t++]=n;return e.length=t,e},remove:function(){var e=this.parentNode;e&&e.removeChild(this)}},i={get nextElementSibling(){return t(this.nextSibling)},get previousElementSibling(){return n(this.previousSibling)}},a={getElementById:function(e){return/[ \t\n\r\f]/.test(e)?null:this.querySelector('[id="'+e+'"]')}};e.ChildNodeInterface=i,e.NonElementParentNodeInterface=a,e.ParentNodeInterface=o}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r.call(this,e)}var n=e.ChildNodeInterface,r=e.wrappers.Node,o=e.enqueueMutation,i=e.mixin,a=e.registerWrapper,s=e.unsafeUnwrap,c=window.CharacterData;t.prototype=Object.create(r.prototype),i(t.prototype,{get nodeValue(){return this.data},set nodeValue(e){this.data=e},get textContent(){return this.data},set textContent(e){this.data=e},get data(){return s(this).data},set data(e){var t=s(this).data;o(this,"characterData",{oldValue:t}),s(this).data=e}}),i(t.prototype,n),a(c,t,document.createTextNode("")),e.wrappers.CharacterData=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){return e>>>0}function n(e){r.call(this,e)}var r=e.wrappers.CharacterData,o=(e.enqueueMutation,e.mixin),i=e.registerWrapper,a=window.Text;n.prototype=Object.create(r.prototype),o(n.prototype,{splitText:function(e){e=t(e);var n=this.data;if(e>n.length)throw new Error("IndexSizeError");var r=n.slice(0,e),o=n.slice(e);this.data=r;var i=this.ownerDocument.createTextNode(o);return this.parentNode&&this.parentNode.insertBefore(i,this.nextSibling),i}}),i(a,n,document.createTextNode("")),e.wrappers.Text=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){return i(e).getAttribute("class")}function n(e,t){a(e,"attributes",{name:"class",namespace:null,oldValue:t})}function r(t){e.invalidateRendererBasedOnAttribute(t,"class")}function o(e,o,i){var a=e.ownerElement_;if(null==a)return o.apply(e,i);var s=t(a),c=o.apply(e,i);return t(a)!==s&&(n(a,s),r(a)),c}if(!window.DOMTokenList)return void console.warn("Missing DOMTokenList prototype, please include a compatible classList polyfill such as http://goo.gl/uTcepH.");var i=e.unsafeUnwrap,a=e.enqueueMutation,s=DOMTokenList.prototype.add;DOMTokenList.prototype.add=function(){o(this,s,arguments)};var c=DOMTokenList.prototype.remove;DOMTokenList.prototype.remove=function(){o(this,c,arguments)};var l=DOMTokenList.prototype.toggle;DOMTokenList.prototype.toggle=function(){return o(this,l,arguments)}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(t,n){var r=t.parentNode;if(r&&r.shadowRoot){var o=e.getRendererForHost(r);o.dependsOnAttribute(n)&&o.invalidate()}}function n(e,t,n){u(e,"attributes",{name:t,namespace:null,oldValue:n})}function r(e){a.call(this,e)}var o=e.ChildNodeInterface,i=e.GetElementsByInterface,a=e.wrappers.Node,s=e.ParentNodeInterface,c=e.SelectorsInterface,l=e.MatchesInterface,u=(e.addWrapNodeListMethod,e.enqueueMutation),d=e.mixin,p=(e.oneOf,e.registerWrapper),h=e.unsafeUnwrap,f=e.wrappers,m=window.Element,w=["matches","mozMatchesSelector","msMatchesSelector","webkitMatchesSelector"].filter(function(e){return m.prototype[e]}),v=w[0],g=m.prototype[v],b=new WeakMap;r.prototype=Object.create(a.prototype),d(r.prototype,{createShadowRoot:function(){var t=new f.ShadowRoot(this);h(this).polymerShadowRoot_=t;var n=e.getRendererForHost(this);return n.invalidate(),t},get shadowRoot(){return h(this).polymerShadowRoot_||null},setAttribute:function(e,r){var o=h(this).getAttribute(e);h(this).setAttribute(e,r),n(this,e,o),t(this,e)},removeAttribute:function(e){var r=h(this).getAttribute(e);h(this).removeAttribute(e),n(this,e,r),t(this,e)},get classList(){var e=b.get(this);if(!e){if(e=h(this).classList,!e)return;e.ownerElement_=this,b.set(this,e)}return e},get className(){return h(this).className},set className(e){this.setAttribute("class",e)},get id(){return h(this).id},set id(e){this.setAttribute("id",e)}}),w.forEach(function(e){"matches"!==e&&(r.prototype[e]=function(e){return this.matches(e)})}),m.prototype.webkitCreateShadowRoot&&(r.prototype.webkitCreateShadowRoot=r.prototype.createShadowRoot),d(r.prototype,o),d(r.prototype,i),d(r.prototype,s),d(r.prototype,c),d(r.prototype,l),p(m,r,document.createElementNS(null,"x")),e.invalidateRendererBasedOnAttribute=t,e.matchesNames=w,e.originalMatches=g,e.wrappers.Element=r}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case'"':return""";case"Â ":return" "}}function n(e){return e.replace(L,t)}function r(e){return e.replace(N,t)}function o(e){for(var t={},n=0;n<e.length;n++)t[e[n]]=!0;return t}function i(e){if(e.namespaceURI!==D)return!0;var t=e.ownerDocument.doctype;return t&&t.publicId&&t.systemId}function a(e,t){switch(e.nodeType){case Node.ELEMENT_NODE:for(var o,a=e.tagName.toLowerCase(),c="<"+a,l=e.attributes,u=0;o=l[u];u++)c+=" "+o.name+'="'+n(o.value)+'"';return C[a]?(i(e)&&(c+="/"),c+">"):c+">"+s(e)+"</"+a+">";case Node.TEXT_NODE:var d=e.data;return t&&j[t.localName]?d:r(d);case Node.COMMENT_NODE:return"<!--"+e.data+"-->";default:throw console.error(e),new Error("not implemented")}}function s(e){e instanceof O.HTMLTemplateElement&&(e=e.content);for(var t="",n=e.firstChild;n;n=n.nextSibling)t+=a(n,e);return t}function c(e,t,n){var r=n||"div";e.textContent="";var o=T(e.ownerDocument.createElement(r));o.innerHTML=t;for(var i;i=o.firstChild;)e.appendChild(M(i))}function l(e){m.call(this,e)}function u(e,t){var n=T(e.cloneNode(!1));n.innerHTML=t;for(var r,o=T(document.createDocumentFragment());r=n.firstChild;)o.appendChild(r);return M(o)}function d(t){return function(){return e.renderAllPending(),S(this)[t]}}function p(e){w(l,e,d(e))}function h(t){Object.defineProperty(l.prototype,t,{get:d(t),set:function(n){e.renderAllPending(),S(this)[t]=n},configurable:!0,enumerable:!0})}function f(t){Object.defineProperty(l.prototype,t,{value:function(){return e.renderAllPending(),S(this)[t].apply(S(this),arguments)},configurable:!0,enumerable:!0})}var m=e.wrappers.Element,w=e.defineGetter,v=e.enqueueMutation,g=e.mixin,b=e.nodesWereAdded,y=e.nodesWereRemoved,E=e.registerWrapper,_=e.snapshotNodeList,S=e.unsafeUnwrap,T=e.unwrap,M=e.wrap,O=e.wrappers,L=/[&\u00A0"]/g,N=/[&\u00A0<>]/g,C=o(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),j=o(["style","script","xmp","iframe","noembed","noframes","plaintext","noscript"]),D="http://www.w3.org/1999/xhtml",H=/MSIE/.test(navigator.userAgent),x=window.HTMLElement,R=window.HTMLTemplateElement;l.prototype=Object.create(m.prototype),g(l.prototype,{get innerHTML(){return s(this)},set innerHTML(e){if(H&&j[this.localName])return void(this.textContent=e);var t=_(this.childNodes);this.invalidateShadowRenderer()?this instanceof O.HTMLTemplateElement?c(this.content,e):c(this,e,this.tagName):!R&&this instanceof O.HTMLTemplateElement?c(this.content,e):S(this).innerHTML=e;var n=_(this.childNodes);v(this,"childList",{addedNodes:n,removedNodes:t}),y(t),b(n,this)},get outerHTML(){return a(this,this.parentNode)},set outerHTML(e){var t=this.parentNode;if(t){t.invalidateShadowRenderer();var n=u(t,e);t.replaceChild(n,this)}},insertAdjacentHTML:function(e,t){var n,r;switch(String(e).toLowerCase()){case"beforebegin":n=this.parentNode,r=this;break;case"afterend":n=this.parentNode,r=this.nextSibling;break;case"afterbegin":n=this,r=this.firstChild;break;case"beforeend":n=this,r=null;break;default:return}var o=u(n,t);n.insertBefore(o,r)},get hidden(){return this.hasAttribute("hidden")},set hidden(e){e?this.setAttribute("hidden",""):this.removeAttribute("hidden")}}),["clientHeight","clientLeft","clientTop","clientWidth","offsetHeight","offsetLeft","offsetTop","offsetWidth","scrollHeight","scrollWidth"].forEach(p),["scrollLeft","scrollTop"].forEach(h),["focus","getBoundingClientRect","getClientRects","scrollIntoView"].forEach(f),E(x,l,document.createElement("b")),e.wrappers.HTMLElement=l,e.getInnerHTML=s,e.setInnerHTML=c}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.unsafeUnwrap,a=e.wrap,s=window.HTMLCanvasElement;t.prototype=Object.create(n.prototype),r(t.prototype,{getContext:function(){var e=i(this).getContext.apply(i(this),arguments);return e&&a(e)}}),o(s,t,document.createElement("canvas")),e.wrappers.HTMLCanvasElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=window.HTMLContentElement;t.prototype=Object.create(n.prototype),r(t.prototype,{constructor:t,get select(){return this.getAttribute("select")},set select(e){this.setAttribute("select",e)},setAttribute:function(e,t){n.prototype.setAttribute.call(this,e,t),"select"===String(e).toLowerCase()&&this.invalidateShadowRenderer(!0)}}),i&&o(i,t),e.wrappers.HTMLContentElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.wrapHTMLCollection,a=e.unwrap,s=window.HTMLFormElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get elements(){return i(a(this).elements)}}),o(s,t,document.createElement("form")),e.wrappers.HTMLFormElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r.call(this,e)}function n(e,t){if(!(this instanceof n))throw new TypeError("DOM object constructor cannot be called as a function.");var o=i(document.createElement("img"));r.call(this,o),a(o,this),void 0!==e&&(o.width=e),void 0!==t&&(o.height=t)}var r=e.wrappers.HTMLElement,o=e.registerWrapper,i=e.unwrap,a=e.rewrap,s=window.HTMLImageElement;t.prototype=Object.create(r.prototype),o(s,t,document.createElement("img")),n.prototype=t.prototype,e.wrappers.HTMLImageElement=t,e.wrappers.Image=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=(e.mixin,e.wrappers.NodeList,e.registerWrapper),o=window.HTMLShadowElement;t.prototype=Object.create(n.prototype),t.prototype.constructor=t,o&&r(o,t),e.wrappers.HTMLShadowElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){if(!e.defaultView)return e;var t=d.get(e);if(!t){for(t=e.implementation.createHTMLDocument("");t.lastChild;)t.removeChild(t.lastChild);d.set(e,t)}return t}function n(e){for(var n,r=t(e.ownerDocument),o=c(r.createDocumentFragment());n=e.firstChild;)o.appendChild(n);return o}function r(e){if(o.call(this,e),!p){var t=n(e);u.set(this,l(t))}}var o=e.wrappers.HTMLElement,i=e.mixin,a=e.registerWrapper,s=e.unsafeUnwrap,c=e.unwrap,l=e.wrap,u=new WeakMap,d=new WeakMap,p=window.HTMLTemplateElement;r.prototype=Object.create(o.prototype),i(r.prototype,{constructor:r,get content(){return p?l(s(this).content):u.get(this)}}),p&&a(p,r),e.wrappers.HTMLTemplateElement=r}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.registerWrapper,o=window.HTMLMediaElement;o&&(t.prototype=Object.create(n.prototype),r(o,t,document.createElement("audio")),e.wrappers.HTMLMediaElement=t)}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r.call(this,e)}function n(e){if(!(this instanceof n))throw new TypeError("DOM object constructor cannot be called as a function.");var t=i(document.createElement("audio"));r.call(this,t),a(t,this),t.setAttribute("preload","auto"),void 0!==e&&t.setAttribute("src",e)}var r=e.wrappers.HTMLMediaElement,o=e.registerWrapper,i=e.unwrap,a=e.rewrap,s=window.HTMLAudioElement;s&&(t.prototype=Object.create(r.prototype),o(s,t,document.createElement("audio")),n.prototype=t.prototype,e.wrappers.HTMLAudioElement=t,e.wrappers.Audio=n)}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){return e.replace(/\s+/g," ").trim()}function n(e){o.call(this,e)}function r(e,t,n,i){if(!(this instanceof r))throw new TypeError("DOM object constructor cannot be called as a function.");var a=c(document.createElement("option"));o.call(this,a),s(a,this),void 0!==e&&(a.text=e),void 0!==t&&a.setAttribute("value",t),n===!0&&a.setAttribute("selected",""),a.selected=i===!0}var o=e.wrappers.HTMLElement,i=e.mixin,a=e.registerWrapper,s=e.rewrap,c=e.unwrap,l=e.wrap,u=window.HTMLOptionElement;n.prototype=Object.create(o.prototype),i(n.prototype,{get text(){return t(this.textContent)},set text(e){this.textContent=t(String(e))},get form(){return l(c(this).form)}}),a(u,n,document.createElement("option")),r.prototype=n.prototype,e.wrappers.HTMLOptionElement=n,e.wrappers.Option=r}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.unwrap,a=e.wrap,s=window.HTMLSelectElement;t.prototype=Object.create(n.prototype),r(t.prototype,{add:function(e,t){"object"==typeof t&&(t=i(t)),i(this).add(i(e),t)},remove:function(e){return void 0===e?void n.prototype.remove.call(this):("object"==typeof e&&(e=i(e)),void i(this).remove(e))},get form(){return a(i(this).form)}}),o(s,t,document.createElement("select")),e.wrappers.HTMLSelectElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.unwrap,a=e.wrap,s=e.wrapHTMLCollection,c=window.HTMLTableElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get caption(){return a(i(this).caption)},createCaption:function(){return a(i(this).createCaption())},get tHead(){return a(i(this).tHead)},createTHead:function(){return a(i(this).createTHead())},createTFoot:function(){return a(i(this).createTFoot())},get tFoot(){return a(i(this).tFoot)},get tBodies(){return s(i(this).tBodies)},createTBody:function(){return a(i(this).createTBody())},get rows(){return s(i(this).rows)},insertRow:function(e){return a(i(this).insertRow(e))}}),o(c,t,document.createElement("table")),e.wrappers.HTMLTableElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.wrapHTMLCollection,a=e.unwrap,s=e.wrap,c=window.HTMLTableSectionElement;t.prototype=Object.create(n.prototype),r(t.prototype,{constructor:t,get rows(){return i(a(this).rows)},insertRow:function(e){return s(a(this).insertRow(e))}}),o(c,t,document.createElement("thead")),e.wrappers.HTMLTableSectionElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.wrapHTMLCollection,a=e.unwrap,s=e.wrap,c=window.HTMLTableRowElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get cells(){return i(a(this).cells)},insertCell:function(e){return s(a(this).insertCell(e))}}),o(c,t,document.createElement("tr")),e.wrappers.HTMLTableRowElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){switch(e.localName){case"content":return new n(e);case"shadow":return new o(e);case"template":return new i(e)}r.call(this,e)}var n=e.wrappers.HTMLContentElement,r=e.wrappers.HTMLElement,o=e.wrappers.HTMLShadowElement,i=e.wrappers.HTMLTemplateElement,a=(e.mixin,e.registerWrapper),s=window.HTMLUnknownElement;t.prototype=Object.create(r.prototype),a(s,t),e.wrappers.HTMLUnknownElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.Element,r=e.wrappers.HTMLElement,o=e.registerWrapper,i=(e.defineWrapGetter,e.unsafeUnwrap),a=e.wrap,s=e.mixin,c="http://www.w3.org/2000/svg",l=window.SVGElement,u=document.createElementNS(c,"title");if(!("classList"in u)){var d=Object.getOwnPropertyDescriptor(n.prototype,"classList");Object.defineProperty(r.prototype,"classList",d),delete n.prototype.classList}t.prototype=Object.create(n.prototype),s(t.prototype,{get ownerSVGElement(){return a(i(this).ownerSVGElement)}}),o(l,t,document.createElementNS(c,"title")),e.wrappers.SVGElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){p.call(this,e)}var n=e.mixin,r=e.registerWrapper,o=e.unwrap,i=e.wrap,a=window.SVGUseElement,s="http://www.w3.org/2000/svg",c=i(document.createElementNS(s,"g")),l=document.createElementNS(s,"use"),u=c.constructor,d=Object.getPrototypeOf(u.prototype),p=d.constructor;t.prototype=Object.create(d),"instanceRoot"in l&&n(t.prototype,{get instanceRoot(){return i(o(this).instanceRoot)},get animatedInstanceRoot(){return i(o(this).animatedInstanceRoot)}}),r(a,t,l),e.wrappers.SVGUseElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.EventTarget,r=e.mixin,o=e.registerWrapper,i=e.unsafeUnwrap,a=e.wrap,s=window.SVGElementInstance;s&&(t.prototype=Object.create(n.prototype),r(t.prototype,{get correspondingElement(){return a(i(this).correspondingElement)},get correspondingUseElement(){return a(i(this).correspondingUseElement)},get parentNode(){return a(i(this).parentNode)},get childNodes(){throw new Error("Not implemented")},get firstChild(){return a(i(this).firstChild)},get lastChild(){return a(i(this).lastChild)},get previousSibling(){return a(i(this).previousSibling)},get nextSibling(){return a(i(this).nextSibling)}}),o(s,t),e.wrappers.SVGElementInstance=t)}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){o(e,this)}var n=e.mixin,r=e.registerWrapper,o=e.setWrapper,i=e.unsafeUnwrap,a=e.unwrap,s=e.unwrapIfNeeded,c=e.wrap,l=window.CanvasRenderingContext2D;n(t.prototype,{get canvas(){return c(i(this).canvas)},drawImage:function(){arguments[0]=s(arguments[0]),i(this).drawImage.apply(i(this),arguments)},createPattern:function(){return arguments[0]=a(arguments[0]),i(this).createPattern.apply(i(this),arguments)}}),r(l,t,document.createElement("canvas").getContext("2d")),e.wrappers.CanvasRenderingContext2D=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){i(e,this)}var n=e.addForwardingProperties,r=e.mixin,o=e.registerWrapper,i=e.setWrapper,a=e.unsafeUnwrap,s=e.unwrapIfNeeded,c=e.wrap,l=window.WebGLRenderingContext;if(l){r(t.prototype,{get canvas(){return c(a(this).canvas)},texImage2D:function(){arguments[5]=s(arguments[5]),a(this).texImage2D.apply(a(this),arguments)},texSubImage2D:function(){arguments[6]=s(arguments[6]),a(this).texSubImage2D.apply(a(this),arguments)}});var u=Object.getPrototypeOf(l.prototype);u!==Object.prototype&&n(u,t.prototype);var d=/WebKit/.test(navigator.userAgent)?{drawingBufferHeight:null,drawingBufferWidth:null}:{};o(l,t,d),e.wrappers.WebGLRenderingContext=t}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.Node,r=e.GetElementsByInterface,o=e.NonElementParentNodeInterface,i=e.ParentNodeInterface,a=e.SelectorsInterface,s=e.mixin,c=e.registerObject,l=e.registerWrapper,u=window.DocumentFragment;t.prototype=Object.create(n.prototype),s(t.prototype,i),s(t.prototype,a),s(t.prototype,r),s(t.prototype,o),l(u,t,document.createDocumentFragment()),e.wrappers.DocumentFragment=t;var d=c(document.createComment(""));e.wrappers.Comment=d}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t=d(u(e).ownerDocument.createDocumentFragment());n.call(this,t),c(t,this);var o=e.shadowRoot;f.set(this,o),this.treeScope_=new r(this,a(o||e)),h.set(this,e)}var n=e.wrappers.DocumentFragment,r=e.TreeScope,o=e.elementFromPoint,i=e.getInnerHTML,a=e.getTreeScope,s=e.mixin,c=e.rewrap,l=e.setInnerHTML,u=e.unsafeUnwrap,d=e.unwrap,p=e.wrap,h=new WeakMap,f=new WeakMap;t.prototype=Object.create(n.prototype),s(t.prototype,{constructor:t,get innerHTML(){return i(this)},set innerHTML(e){l(this,e),this.invalidateShadowRenderer()},get olderShadowRoot(){return f.get(this)||null},get host(){return h.get(this)||null},invalidateShadowRenderer:function(){return h.get(this).invalidateShadowRenderer()},elementFromPoint:function(e,t){return o(this,this.ownerDocument,e,t)},getSelection:function(){return document.getSelection()},get activeElement(){var e=d(this).ownerDocument.activeElement;if(!e||!e.nodeType)return null;for(var t=p(e);!this.contains(t);){for(;t.parentNode;)t=t.parentNode;if(!t.host)return null;t=t.host}return t}}),e.wrappers.ShadowRoot=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t=d(e).root;return t instanceof h?t.host:null}function n(t,n){if(t.shadowRoot){n=Math.min(t.childNodes.length-1,n);var r=t.childNodes[n];if(r){var o=e.getDestinationInsertionPoints(r);if(o.length>0){var i=o[0].parentNode;i.nodeType==Node.ELEMENT_NODE&&(t=i)}}}return t}function r(e){return e=u(e),t(e)||e}function o(e){a(e,this)}var i=e.registerWrapper,a=e.setWrapper,s=e.unsafeUnwrap,c=e.unwrap,l=e.unwrapIfNeeded,u=e.wrap,d=e.getTreeScope,p=window.Range,h=e.wrappers.ShadowRoot;o.prototype={get startContainer(){return r(s(this).startContainer)},get endContainer(){return r(s(this).endContainer)},get commonAncestorContainer(){return r(s(this).commonAncestorContainer)},setStart:function(e,t){e=n(e,t),s(this).setStart(l(e),t)},setEnd:function(e,t){e=n(e,t),s(this).setEnd(l(e),t)},setStartBefore:function(e){s(this).setStartBefore(l(e))},setStartAfter:function(e){s(this).setStartAfter(l(e))},setEndBefore:function(e){s(this).setEndBefore(l(e))},setEndAfter:function(e){s(this).setEndAfter(l(e))},selectNode:function(e){s(this).selectNode(l(e))},selectNodeContents:function(e){s(this).selectNodeContents(l(e))},compareBoundaryPoints:function(e,t){return s(this).compareBoundaryPoints(e,c(t))},extractContents:function(){return u(s(this).extractContents())},cloneContents:function(){return u(s(this).cloneContents())},insertNode:function(e){s(this).insertNode(l(e))},surroundContents:function(e){s(this).surroundContents(l(e))},cloneRange:function(){return u(s(this).cloneRange())},isPointInRange:function(e,t){return s(this).isPointInRange(l(e),t)},comparePoint:function(e,t){return s(this).comparePoint(l(e),t)},intersectsNode:function(e){return s(this).intersectsNode(l(e))},toString:function(){return s(this).toString()}},p.prototype.createContextualFragment&&(o.prototype.createContextualFragment=function(e){return u(s(this).createContextualFragment(e))}),i(window.Range,o,document.createRange()),e.wrappers.Range=o}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){e.previousSibling_=e.previousSibling,e.nextSibling_=e.nextSibling,e.parentNode_=e.parentNode}function n(n,o,i){var a=x(n),s=x(o),c=i?x(i):null;if(r(o),t(o),i)n.firstChild===i&&(n.firstChild_=i),i.previousSibling_=i.previousSibling;else{n.lastChild_=n.lastChild,n.lastChild===n.firstChild&&(n.firstChild_=n.firstChild);var l=R(a.lastChild);l&&(l.nextSibling_=l.nextSibling)}e.originalInsertBefore.call(a,s,c)}function r(n){var r=x(n),o=r.parentNode;if(o){var i=R(o);t(n),n.previousSibling&&(n.previousSibling.nextSibling_=n),n.nextSibling&&(n.nextSibling.previousSibling_=n),i.lastChild===n&&(i.lastChild_=n),i.firstChild===n&&(i.firstChild_=n),e.originalRemoveChild.call(o,r)}}function o(e){P.set(e,[])}function i(e){var t=P.get(e);return t||P.set(e,t=[]),t}function a(e){for(var t=[],n=0,r=e.firstChild;r;r=r.nextSibling)t[n++]=r;return t}function s(){for(var e=0;e<F.length;e++){var t=F[e],n=t.parentRenderer;n&&n.dirty||t.render()}F=[]}function c(){T=null,s()}function l(e){var t=A.get(e);return t||(t=new h(e),A.set(e,t)),t}function u(e){var t=j(e).root;return t instanceof C?t:null}function d(e){return l(e.host)}function p(e){this.skip=!1,this.node=e,this.childNodes=[]}function h(e){this.host=e,this.dirty=!1,this.invalidateAttributes(),this.associateNode(e)}function f(e){for(var t=[],n=e.firstChild;n;n=n.nextSibling)E(n)?t.push.apply(t,i(n)):t.push(n);return t}function m(e){if(e instanceof L)return e;if(e instanceof O)return null;for(var t=e.firstChild;t;t=t.nextSibling){var n=m(t);if(n)return n}return null}function w(e,t){i(t).push(e);var n=k.get(e);n?n.push(t):k.set(e,[t])}function v(e){return k.get(e)}function g(e){k.set(e,void 0)}function b(e,t){var n=t.getAttribute("select");if(!n)return!0;if(n=n.trim(),!n)return!0;if(!(e instanceof M))return!1;if(!q.test(n))return!1;try{return e.matches(n)}catch(r){return!1}}function y(e,t){var n=v(t);return n&&n[n.length-1]===e}function E(e){return e instanceof O||e instanceof L}function _(e){return e.shadowRoot}function S(e){for(var t=[],n=e.shadowRoot;n;n=n.olderShadowRoot)t.push(n);return t}var T,M=e.wrappers.Element,O=e.wrappers.HTMLContentElement,L=e.wrappers.HTMLShadowElement,N=e.wrappers.Node,C=e.wrappers.ShadowRoot,j=(e.assert,e.getTreeScope),D=(e.mixin,e.oneOf),H=e.unsafeUnwrap,x=e.unwrap,R=e.wrap,I=e.ArraySplice,P=new WeakMap,k=new WeakMap,A=new WeakMap,W=D(window,["requestAnimationFrame","mozRequestAnimationFrame","webkitRequestAnimationFrame","setTimeout"]),F=[],U=new I;U.equals=function(e,t){return x(e.node)===t},p.prototype={append:function(e){var t=new p(e);return this.childNodes.push(t),t},sync:function(e){if(!this.skip){for(var t=this.node,o=this.childNodes,i=a(x(t)),s=e||new WeakMap,c=U.calculateSplices(o,i),l=0,u=0,d=0,p=0;p<c.length;p++){for(var h=c[p];d<h.index;d++)u++,o[l++].sync(s);for(var f=h.removed.length,m=0;m<f;m++){var w=R(i[u++]);s.get(w)||r(w)}for(var v=h.addedCount,g=i[u]&&R(i[u]),m=0;m<v;m++){var b=o[l++],y=b.node;n(t,y,g),s.set(y,!0),b.sync(s)}d+=v}for(var p=d;p<o.length;p++)o[p].sync(s)}}},h.prototype={render:function(e){if(this.dirty){this.invalidateAttributes();var t=this.host;this.distribution(t);var n=e||new p(t);this.buildRenderTree(n,t);var r=!e;r&&n.sync(),this.dirty=!1}},get parentRenderer(){return j(this.host).renderer},invalidate:function(){if(!this.dirty){this.dirty=!0;var e=this.parentRenderer;if(e&&e.invalidate(),F.push(this),T)return;T=window[W](c,0)}},distribution:function(e){this.resetAllSubtrees(e),this.distributionResolution(e)},resetAll:function(e){E(e)?o(e):g(e),this.resetAllSubtrees(e)},resetAllSubtrees:function(e){for(var t=e.firstChild;t;t=t.nextSibling)this.resetAll(t);e.shadowRoot&&this.resetAll(e.shadowRoot),e.olderShadowRoot&&this.resetAll(e.olderShadowRoot)},distributionResolution:function(e){if(_(e)){for(var t=e,n=f(t),r=S(t),o=0;o<r.length;o++)this.poolDistribution(r[o],n);for(var o=r.length-1;o>=0;o--){var i=r[o],a=m(i);if(a){var s=i.olderShadowRoot;s&&(n=f(s));for(var c=0;c<n.length;c++)w(n[c],a)}this.distributionResolution(i)}}for(var l=e.firstChild;l;l=l.nextSibling)this.distributionResolution(l)},poolDistribution:function(e,t){if(!(e instanceof L))if(e instanceof O){var n=e;this.updateDependentAttributes(n.getAttribute("select"));for(var r=!1,o=0;o<t.length;o++){var e=t[o];e&&b(e,n)&&(w(e,n),t[o]=void 0,r=!0)}if(!r)for(var i=n.firstChild;i;i=i.nextSibling)w(i,n)}else for(var i=e.firstChild;i;i=i.nextSibling)this.poolDistribution(i,t)},buildRenderTree:function(e,t){for(var n=this.compose(t),r=0;r<n.length;r++){var o=n[r],i=e.append(o);this.buildRenderTree(i,o)}if(_(t)){var a=l(t);a.dirty=!1}},compose:function(e){for(var t=[],n=e.shadowRoot||e,r=n.firstChild;r;r=r.nextSibling)if(E(r)){this.associateNode(n);for(var o=i(r),a=0;a<o.length;a++){var s=o[a];y(r,s)&&t.push(s)}}else t.push(r);return t},invalidateAttributes:function(){this.attributes=Object.create(null)},updateDependentAttributes:function(e){if(e){var t=this.attributes;/\.\w+/.test(e)&&(t["class"]=!0),/#\w+/.test(e)&&(t.id=!0),e.replace(/\[\s*([^\s=\|~\]]+)/g,function(e,n){t[n]=!0})}},dependsOnAttribute:function(e){return this.attributes[e]},associateNode:function(e){H(e).polymerShadowRenderer_=this}};var q=/^(:not\()?[*.#[a-zA-Z_|]/;N.prototype.invalidateShadowRenderer=function(e){var t=H(this).polymerShadowRenderer_;return!!t&&(t.invalidate(),!0)},O.prototype.getDistributedNodes=L.prototype.getDistributedNodes=function(){return s(),i(this)},M.prototype.getDestinationInsertionPoints=function(){return s(),v(this)||[]},O.prototype.nodeIsInserted_=L.prototype.nodeIsInserted_=function(){this.invalidateShadowRenderer();var e,t=u(this);t&&(e=d(t)),H(this).polymerShadowRenderer_=e,e&&e.invalidate()},e.getRendererForHost=l,e.getShadowTrees=S,e.renderAllPending=s,e.getDestinationInsertionPoints=v,e.visual={insertBefore:n,remove:r}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(t){if(window[t]){r(!e.wrappers[t]);var c=function(e){n.call(this,e)};c.prototype=Object.create(n.prototype),o(c.prototype,{get form(){return s(a(this).form)}}),i(window[t],c,document.createElement(t.slice(4,-7))),e.wrappers[t]=c}}var n=e.wrappers.HTMLElement,r=e.assert,o=e.mixin,i=e.registerWrapper,a=e.unwrap,s=e.wrap,c=["HTMLButtonElement","HTMLFieldSetElement","HTMLInputElement","HTMLKeygenElement","HTMLLabelElement","HTMLLegendElement","HTMLObjectElement","HTMLOutputElement","HTMLTextAreaElement"];c.forEach(t)}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r(e,this)}var n=e.registerWrapper,r=e.setWrapper,o=e.unsafeUnwrap,i=e.unwrap,a=e.unwrapIfNeeded,s=e.wrap,c=window.Selection;t.prototype={get anchorNode(){return s(o(this).anchorNode)},get focusNode(){return s(o(this).focusNode)},addRange:function(e){o(this).addRange(a(e))},collapse:function(e,t){o(this).collapse(a(e),t)},containsNode:function(e,t){return o(this).containsNode(a(e),t)},getRangeAt:function(e){return s(o(this).getRangeAt(e))},removeRange:function(e){o(this).removeRange(i(e))},selectAllChildren:function(e){o(this).selectAllChildren(e instanceof ShadowRoot?o(e.host):a(e))},toString:function(){return o(this).toString()}},c.prototype.extend&&(t.prototype.extend=function(e,t){o(this).extend(a(e),t)}),n(window.Selection,t,window.getSelection()),e.wrappers.Selection=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r(e,this)}var n=e.registerWrapper,r=e.setWrapper,o=e.unsafeUnwrap,i=e.unwrapIfNeeded,a=e.wrap,s=window.TreeWalker;t.prototype={get root(){return a(o(this).root)},get currentNode(){return a(o(this).currentNode)},set currentNode(e){o(this).currentNode=i(e)},get filter(){return o(this).filter},parentNode:function(){return a(o(this).parentNode())},firstChild:function(){return a(o(this).firstChild())},lastChild:function(){return a(o(this).lastChild())},previousSibling:function(){return a(o(this).previousSibling())},previousNode:function(){return a(o(this).previousNode())},nextNode:function(){return a(o(this).nextNode())}},n(s,t),e.wrappers.TreeWalker=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){u.call(this,e),this.treeScope_=new w(this,null)}function n(e){var n=document[e];t.prototype[e]=function(){return j(n.apply(N(this),arguments))}}function r(e,t){x.call(N(t),C(e)),o(e,t)}function o(e,t){e.shadowRoot&&t.adoptNode(e.shadowRoot),e instanceof m&&i(e,t);for(var n=e.firstChild;n;n=n.nextSibling)o(n,t)}function i(e,t){var n=e.olderShadowRoot;n&&t.adoptNode(n)}function a(e){L(e,this)}function s(e,t){var n=document.implementation[t];e.prototype[t]=function(){ +return j(n.apply(N(this),arguments))}}function c(e,t){var n=document.implementation[t];e.prototype[t]=function(){return n.apply(N(this),arguments)}}var l=e.GetElementsByInterface,u=e.wrappers.Node,d=e.ParentNodeInterface,p=e.NonElementParentNodeInterface,h=e.wrappers.Selection,f=e.SelectorsInterface,m=e.wrappers.ShadowRoot,w=e.TreeScope,v=e.cloneNode,g=e.defineGetter,b=e.defineWrapGetter,y=e.elementFromPoint,E=e.forwardMethodsToWrapper,_=e.matchesNames,S=e.mixin,T=e.registerWrapper,M=e.renderAllPending,O=e.rewrap,L=e.setWrapper,N=e.unsafeUnwrap,C=e.unwrap,j=e.wrap,D=e.wrapEventTargetMethods,H=(e.wrapNodeList,new WeakMap);t.prototype=Object.create(u.prototype),b(t,"documentElement"),b(t,"body"),b(t,"head"),g(t,"activeElement",function(){var e=C(this).activeElement;if(!e||!e.nodeType)return null;for(var t=j(e);!this.contains(t);){for(;t.parentNode;)t=t.parentNode;if(!t.host)return null;t=t.host}return t}),["createComment","createDocumentFragment","createElement","createElementNS","createEvent","createEventNS","createRange","createTextNode"].forEach(n);var x=document.adoptNode,R=document.getSelection;S(t.prototype,{adoptNode:function(e){return e.parentNode&&e.parentNode.removeChild(e),r(e,this),e},elementFromPoint:function(e,t){return y(this,this,e,t)},importNode:function(e,t){return v(e,t,N(this))},getSelection:function(){return M(),new h(R.call(C(this)))},getElementsByName:function(e){return f.querySelectorAll.call(this,"[name="+JSON.stringify(String(e))+"]")}});var I=document.createTreeWalker,P=e.wrappers.TreeWalker;if(t.prototype.createTreeWalker=function(e,t,n,r){var o=null;return n&&(n.acceptNode&&"function"==typeof n.acceptNode?o={acceptNode:function(e){return n.acceptNode(j(e))}}:"function"==typeof n&&(o=function(e){return n(j(e))})),new P(I.call(C(this),C(e),t,o,r))},document.registerElement){var k=document.registerElement;t.prototype.registerElement=function(t,n){function r(e){return e?void L(e,this):i?document.createElement(i,t):document.createElement(t)}var o,i;if(void 0!==n&&(o=n.prototype,i=n["extends"]),o||(o=Object.create(HTMLElement.prototype)),e.nativePrototypeTable.get(o))throw new Error("NotSupportedError");for(var a,s=Object.getPrototypeOf(o),c=[];s&&!(a=e.nativePrototypeTable.get(s));)c.push(s),s=Object.getPrototypeOf(s);if(!a)throw new Error("NotSupportedError");for(var l=Object.create(a),u=c.length-1;u>=0;u--)l=Object.create(l);["createdCallback","attachedCallback","detachedCallback","attributeChangedCallback"].forEach(function(e){var t=o[e];t&&(l[e]=function(){j(this)instanceof r||O(this),t.apply(j(this),arguments)})});var d={prototype:l};i&&(d["extends"]=i),r.prototype=o,r.prototype.constructor=r,e.constructorTable.set(l,r),e.nativePrototypeTable.set(o,l);k.call(C(this),t,d);return r},E([window.HTMLDocument||window.Document],["registerElement"])}E([window.HTMLBodyElement,window.HTMLDocument||window.Document,window.HTMLHeadElement,window.HTMLHtmlElement],["appendChild","compareDocumentPosition","contains","getElementsByClassName","getElementsByTagName","getElementsByTagNameNS","insertBefore","querySelector","querySelectorAll","removeChild","replaceChild"]),E([window.HTMLBodyElement,window.HTMLHeadElement,window.HTMLHtmlElement],_),E([window.HTMLDocument||window.Document],["adoptNode","importNode","contains","createComment","createDocumentFragment","createElement","createElementNS","createEvent","createEventNS","createRange","createTextNode","createTreeWalker","elementFromPoint","getElementById","getElementsByName","getSelection"]),S(t.prototype,l),S(t.prototype,d),S(t.prototype,f),S(t.prototype,p),S(t.prototype,{get implementation(){var e=H.get(this);return e?e:(e=new a(C(this).implementation),H.set(this,e),e)},get defaultView(){return j(C(this).defaultView)}}),T(window.Document,t,document.implementation.createHTMLDocument("")),window.HTMLDocument&&T(window.HTMLDocument,t),D([window.HTMLBodyElement,window.HTMLDocument||window.Document,window.HTMLHeadElement]);var A=document.implementation.createDocument;a.prototype.createDocument=function(){return arguments[2]=C(arguments[2]),j(A.apply(N(this),arguments))},s(a,"createDocumentType"),s(a,"createHTMLDocument"),c(a,"hasFeature"),T(window.DOMImplementation,a),E([window.DOMImplementation],["createDocument","createDocumentType","createHTMLDocument","hasFeature"]),e.adoptNodeNoRemove=r,e.wrappers.DOMImplementation=a,e.wrappers.Document=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.EventTarget,r=e.wrappers.Selection,o=e.mixin,i=e.registerWrapper,a=e.renderAllPending,s=e.unwrap,c=e.unwrapIfNeeded,l=e.wrap,u=window.Window,d=window.getComputedStyle,p=window.getDefaultComputedStyle,h=window.getSelection;t.prototype=Object.create(n.prototype),u.prototype.getComputedStyle=function(e,t){return l(this||window).getComputedStyle(c(e),t)},p&&(u.prototype.getDefaultComputedStyle=function(e,t){return l(this||window).getDefaultComputedStyle(c(e),t)}),u.prototype.getSelection=function(){return l(this||window).getSelection()},delete window.getComputedStyle,delete window.getDefaultComputedStyle,delete window.getSelection,["addEventListener","removeEventListener","dispatchEvent"].forEach(function(e){u.prototype[e]=function(){var t=l(this||window);return t[e].apply(t,arguments)},delete window[e]}),o(t.prototype,{getComputedStyle:function(e,t){return a(),d.call(s(this),c(e),t)},getSelection:function(){return a(),new r(h.call(s(this)))},get document(){return l(s(this).document)}}),p&&(t.prototype.getDefaultComputedStyle=function(e,t){return a(),p.call(s(this),c(e),t)}),i(u,t,window),e.wrappers.Window=t}(window.ShadowDOMPolyfill),function(e){"use strict";var t=e.unwrap,n=window.DataTransfer||window.Clipboard,r=n.prototype.setDragImage;r&&(n.prototype.setDragImage=function(e,n,o){r.call(this,t(e),n,o)})}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t;t=e instanceof i?e:new i(e&&o(e)),r(t,this)}var n=e.registerWrapper,r=e.setWrapper,o=e.unwrap,i=window.FormData;i&&(n(i,t,new i),e.wrappers.FormData=t)}(window.ShadowDOMPolyfill),function(e){"use strict";var t=e.unwrapIfNeeded,n=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.send=function(e){return n.call(this,t(e))}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t=n[e],r=window[t];if(r){var o=document.createElement(e),i=o.constructor;window[t]=i}}var n=(e.isWrapperFor,{a:"HTMLAnchorElement",area:"HTMLAreaElement",audio:"HTMLAudioElement",base:"HTMLBaseElement",body:"HTMLBodyElement",br:"HTMLBRElement",button:"HTMLButtonElement",canvas:"HTMLCanvasElement",caption:"HTMLTableCaptionElement",col:"HTMLTableColElement",content:"HTMLContentElement",data:"HTMLDataElement",datalist:"HTMLDataListElement",del:"HTMLModElement",dir:"HTMLDirectoryElement",div:"HTMLDivElement",dl:"HTMLDListElement",embed:"HTMLEmbedElement",fieldset:"HTMLFieldSetElement",font:"HTMLFontElement",form:"HTMLFormElement",frame:"HTMLFrameElement",frameset:"HTMLFrameSetElement",h1:"HTMLHeadingElement",head:"HTMLHeadElement",hr:"HTMLHRElement",html:"HTMLHtmlElement",iframe:"HTMLIFrameElement",img:"HTMLImageElement",input:"HTMLInputElement",keygen:"HTMLKeygenElement",label:"HTMLLabelElement",legend:"HTMLLegendElement",li:"HTMLLIElement",link:"HTMLLinkElement",map:"HTMLMapElement",marquee:"HTMLMarqueeElement",menu:"HTMLMenuElement",menuitem:"HTMLMenuItemElement",meta:"HTMLMetaElement",meter:"HTMLMeterElement",object:"HTMLObjectElement",ol:"HTMLOListElement",optgroup:"HTMLOptGroupElement",option:"HTMLOptionElement",output:"HTMLOutputElement",p:"HTMLParagraphElement",param:"HTMLParamElement",pre:"HTMLPreElement",progress:"HTMLProgressElement",q:"HTMLQuoteElement",script:"HTMLScriptElement",select:"HTMLSelectElement",shadow:"HTMLShadowElement",source:"HTMLSourceElement",span:"HTMLSpanElement",style:"HTMLStyleElement",table:"HTMLTableElement",tbody:"HTMLTableSectionElement",template:"HTMLTemplateElement",textarea:"HTMLTextAreaElement",thead:"HTMLTableSectionElement",time:"HTMLTimeElement",title:"HTMLTitleElement",tr:"HTMLTableRowElement",track:"HTMLTrackElement",ul:"HTMLUListElement",video:"HTMLVideoElement"});Object.keys(n).forEach(t),Object.getOwnPropertyNames(e.wrappers).forEach(function(t){window[t]=e.wrappers[t]})}(window.ShadowDOMPolyfill),function(e){function t(e,t){var n="";return Array.prototype.forEach.call(e,function(e){n+=e.textContent+"\n\n"}),t||(n=n.replace(d,"")),n}function n(e){var t=document.createElement("style");return t.textContent=e,t}function r(e){var t=n(e);document.head.appendChild(t);var r=[];if(t.sheet)try{r=t.sheet.cssRules}catch(o){}else console.warn("sheet not found",t);return t.parentNode.removeChild(t),r}function o(){C.initialized=!0,document.body.appendChild(C);var e=C.contentDocument,t=e.createElement("base");t.href=document.baseURI,e.head.appendChild(t)}function i(e){C.initialized||o(),document.body.appendChild(C),e(C.contentDocument),document.body.removeChild(C)}function a(e,t){if(t){var o;if(e.match("@import")&&D){var a=n(e);i(function(e){e.head.appendChild(a.impl),o=Array.prototype.slice.call(a.sheet.cssRules,0),t(o)})}else o=r(e),t(o)}}function s(e){e&&l().appendChild(document.createTextNode(e))}function c(e,t){var r=n(e);r.setAttribute(t,""),r.setAttribute(x,""),document.head.appendChild(r)}function l(){return j||(j=document.createElement("style"),j.setAttribute(x,""),j[x]=!0),j}var u={strictStyling:!1,registry:{},shimStyling:function(e,n,r){var o=this.prepareRoot(e,n,r),i=this.isTypeExtension(r),a=this.makeScopeSelector(n,i),s=t(o,!0);s=this.scopeCssText(s,a),e&&(e.shimmedStyle=s),this.addCssToDocument(s,n)},shimStyle:function(e,t){return this.shimCssText(e.textContent,t)},shimCssText:function(e,t){return e=this.insertDirectives(e),this.scopeCssText(e,t)},makeScopeSelector:function(e,t){return e?t?"[is="+e+"]":e:""},isTypeExtension:function(e){return e&&e.indexOf("-")<0},prepareRoot:function(e,t,n){var r=this.registerRoot(e,t,n);return this.replaceTextInStyles(r.rootStyles,this.insertDirectives),this.removeStyles(e,r.rootStyles),this.strictStyling&&this.applyScopeToContent(e,t),r.scopeStyles},removeStyles:function(e,t){for(var n,r=0,o=t.length;r<o&&(n=t[r]);r++)n.parentNode.removeChild(n)},registerRoot:function(e,t,n){var r=this.registry[t]={root:e,name:t,extendsName:n},o=this.findStyles(e);r.rootStyles=o,r.scopeStyles=r.rootStyles;var i=this.registry[r.extendsName];return i&&(r.scopeStyles=i.scopeStyles.concat(r.scopeStyles)),r},findStyles:function(e){if(!e)return[];var t=e.querySelectorAll("style");return Array.prototype.filter.call(t,function(e){return!e.hasAttribute(R)})},applyScopeToContent:function(e,t){e&&(Array.prototype.forEach.call(e.querySelectorAll("*"),function(e){e.setAttribute(t,"")}),Array.prototype.forEach.call(e.querySelectorAll("template"),function(e){this.applyScopeToContent(e.content,t)},this))},insertDirectives:function(e){return e=this.insertPolyfillDirectivesInCssText(e),this.insertPolyfillRulesInCssText(e)},insertPolyfillDirectivesInCssText:function(e){return e=e.replace(p,function(e,t){return t.slice(0,-2)+"{"}),e.replace(h,function(e,t){return t+" {"})},insertPolyfillRulesInCssText:function(e){return e=e.replace(f,function(e,t){return t.slice(0,-1)}),e.replace(m,function(e,t,n,r){var o=e.replace(t,"").replace(n,"");return r+o})},scopeCssText:function(e,t){var n=this.extractUnscopedRulesFromCssText(e);if(e=this.insertPolyfillHostInCssText(e),e=this.convertColonHost(e),e=this.convertColonHostContext(e),e=this.convertShadowDOMSelectors(e),t){var e,r=this;a(e,function(n){e=r.scopeRules(n,t)})}return e=e+"\n"+n,e.trim()},extractUnscopedRulesFromCssText:function(e){for(var t,n="";t=w.exec(e);)n+=t[1].slice(0,-1)+"\n\n";for(;t=v.exec(e);)n+=t[0].replace(t[2],"").replace(t[1],t[3])+"\n\n";return n},convertColonHost:function(e){return this.convertColonRule(e,E,this.colonHostPartReplacer)},convertColonHostContext:function(e){return this.convertColonRule(e,_,this.colonHostContextPartReplacer)},convertColonRule:function(e,t,n){return e.replace(t,function(e,t,r,o){if(t=O,r){for(var i,a=r.split(","),s=[],c=0,l=a.length;c<l&&(i=a[c]);c++)i=i.trim(),s.push(n(t,i,o));return s.join(",")}return t+o})},colonHostContextPartReplacer:function(e,t,n){return t.match(g)?this.colonHostPartReplacer(e,t,n):e+t+n+", "+t+" "+e+n},colonHostPartReplacer:function(e,t,n){return e+t.replace(g,"")+n},convertShadowDOMSelectors:function(e){for(var t=0;t<N.length;t++)e=e.replace(N[t]," ");return e},scopeRules:function(e,t){var n="";return e&&Array.prototype.forEach.call(e,function(e){if(e.selectorText&&e.style&&void 0!==e.style.cssText)n+=this.scopeSelector(e.selectorText,t,this.strictStyling)+" {\n\t",n+=this.propertiesFromRule(e)+"\n}\n\n";else if(e.type===CSSRule.MEDIA_RULE)n+="@media "+e.media.mediaText+" {\n",n+=this.scopeRules(e.cssRules,t),n+="\n}\n\n";else try{e.cssText&&(n+=e.cssText+"\n\n")}catch(r){e.type===CSSRule.KEYFRAMES_RULE&&e.cssRules&&(n+=this.ieSafeCssTextFromKeyFrameRule(e))}},this),n},ieSafeCssTextFromKeyFrameRule:function(e){var t="@keyframes "+e.name+" {";return Array.prototype.forEach.call(e.cssRules,function(e){t+=" "+e.keyText+" {"+e.style.cssText+"}"}),t+=" }"},scopeSelector:function(e,t,n){var r=[],o=e.split(",");return o.forEach(function(e){e=e.trim(),this.selectorNeedsScoping(e,t)&&(e=n&&!e.match(O)?this.applyStrictSelectorScope(e,t):this.applySelectorScope(e,t)),r.push(e)},this),r.join(", ")},selectorNeedsScoping:function(e,t){if(Array.isArray(t))return!0;var n=this.makeScopeMatcher(t);return!e.match(n)},makeScopeMatcher:function(e){return e=e.replace(/\[/g,"\\[").replace(/\]/g,"\\]"),new RegExp("^("+e+")"+S,"m")},applySelectorScope:function(e,t){return Array.isArray(t)?this.applySelectorScopeList(e,t):this.applySimpleSelectorScope(e,t)},applySelectorScopeList:function(e,t){for(var n,r=[],o=0;n=t[o];o++)r.push(this.applySimpleSelectorScope(e,n));return r.join(", ")},applySimpleSelectorScope:function(e,t){return e.match(L)?(e=e.replace(O,t),e.replace(L,t+" ")):t+" "+e},applyStrictSelectorScope:function(e,t){t=t.replace(/\[is=([^\]]*)\]/g,"$1");var n=[" ",">","+","~"],r=e,o="["+t+"]";return n.forEach(function(e){var t=r.split(e);r=t.map(function(e){var t=e.trim().replace(L,"");return t&&n.indexOf(t)<0&&t.indexOf(o)<0&&(e=t.replace(/([^:]*)(:*)(.*)/,"$1"+o+"$2$3")),e}).join(e)}),r},insertPolyfillHostInCssText:function(e){return e.replace(M,b).replace(T,g)},propertiesFromRule:function(e){var t=e.style.cssText;e.style.content&&!e.style.content.match(/['"]+|attr/)&&(t=t.replace(/content:[^;]*;/g,"content: '"+e.style.content+"';"));var n=e.style;for(var r in n)"initial"===n[r]&&(t+=r+": initial; ");return t},replaceTextInStyles:function(e,t){e&&t&&(e instanceof Array||(e=[e]),Array.prototype.forEach.call(e,function(e){e.textContent=t.call(this,e.textContent)},this))},addCssToDocument:function(e,t){e.match("@import")?c(e,t):s(e)}},d=/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//gim,p=/\/\*\s*@polyfill ([^*]*\*+([^\/*][^*]*\*+)*\/)([^{]*?){/gim,h=/polyfill-next-selector[^}]*content\:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim,f=/\/\*\s@polyfill-rule([^*]*\*+([^\/*][^*]*\*+)*)\//gim,m=/(polyfill-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,w=/\/\*\s@polyfill-unscoped-rule([^*]*\*+([^\/*][^*]*\*+)*)\//gim,v=/(polyfill-unscoped-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,g="-shadowcsshost",b="-shadowcsscontext",y=")(?:\\(((?:\\([^)(]*\\)|[^)(]*)+?)\\))?([^,{]*)",E=new RegExp("("+g+y,"gim"),_=new RegExp("("+b+y,"gim"),S="([>\\s~+[.,{:][\\s\\S]*)?$",T=/\:host/gim,M=/\:host-context/gim,O=g+"-no-combinator",L=new RegExp(g,"gim"),N=(new RegExp(b,"gim"),[/>>>/g,/::shadow/g,/::content/g,/\/deep\//g,/\/shadow\//g,/\/shadow-deep\//g,/\^\^/g,/\^(?!=)/g]),C=document.createElement("iframe");C.style.display="none";var j,D=navigator.userAgent.match("Chrome"),H="shim-shadowdom",x="shim-shadowdom-css",R="no-shim";if(window.ShadowDOMPolyfill){s("style { display: none !important; }\n");var I=ShadowDOMPolyfill.wrap(document),P=I.querySelector("head");P.insertBefore(l(),P.childNodes[0]),document.addEventListener("DOMContentLoaded",function(){e.urlResolver;if(window.HTMLImports&&!HTMLImports.useNative){var t="link[rel=stylesheet]["+H+"]",n="style["+H+"]";HTMLImports.importer.documentPreloadSelectors+=","+t,HTMLImports.importer.importsPreloadSelectors+=","+t,HTMLImports.parser.documentSelectors=[HTMLImports.parser.documentSelectors,t,n].join(",");var r=HTMLImports.parser.parseGeneric;HTMLImports.parser.parseGeneric=function(e){if(!e[x]){var t=e.__importElement||e;if(!t.hasAttribute(H))return void r.call(this,e);e.__resource&&(t=e.ownerDocument.createElement("style"),t.textContent=e.__resource),HTMLImports.path.resolveUrlsInStyle(t,e.href),t.textContent=u.shimStyle(t),t.removeAttribute(H,""),t.setAttribute(x,""),t[x]=!0,t.parentNode!==P&&(e.parentNode===P?P.replaceChild(t,e):this.addElementToDocument(t)),t.__importParsed=!0,this.markParsingComplete(e),this.parseNext()}};var o=HTMLImports.parser.hasResource;HTMLImports.parser.hasResource=function(e){return"link"===e.localName&&"stylesheet"===e.rel&&e.hasAttribute(H)?e.__resource:o.call(this,e)}}})}e.ShadowCSS=u}(window.WebComponents)),function(e){window.ShadowDOMPolyfill?(window.wrap=ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}}(window.WebComponents),function(e){"use strict";function t(e){return void 0!==p[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&t<127&&[34,35,60,62,63,96].indexOf(t)==-1?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&t<127&&[34,35,60,62,96].indexOf(t)==-1?e:encodeURIComponent(e)}function a(e,a,s){function c(e){b.push(e)}var l=a||"scheme start",u=0,d="",v=!1,g=!1,b=[];e:for(;(e[u-1]!=f||0==u)&&!this._isInvalid;){var y=e[u];switch(l){case"scheme start":if(!y||!m.test(y)){if(a){c("Invalid scheme.");break e}d="",l="no scheme";continue}d+=y.toLowerCase(),l="scheme";break;case"scheme":if(y&&w.test(y))d+=y.toLowerCase();else{if(":"!=y){if(a){if(f==y)break e;c("Code point not allowed in scheme: "+y);break e}d="",u=0,l="no scheme";continue}if(this._scheme=d,d="",a)break e;t(this._scheme)&&(this._isRelative=!0),l="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==y?(this._query="?",l="query"):"#"==y?(this._fragment="#",l="fragment"):f!=y&&"\t"!=y&&"\n"!=y&&"\r"!=y&&(this._schemeData+=o(y));break;case"no scheme":if(s&&t(s._scheme)){l="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=y||"/"!=e[u+1]){c("Expected /, got: "+y),l="relative";continue}l="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),f==y){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==y||"\\"==y)"\\"==y&&c("\\ is an invalid code point."),l="relative slash";else if("?"==y)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,l="query";else{if("#"!=y){var E=e[u+1],_=e[u+2];("file"!=this._scheme||!m.test(y)||":"!=E&&"|"!=E||f!=_&&"/"!=_&&"\\"!=_&&"?"!=_&&"#"!=_)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),l="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,l="fragment"}break;case"relative slash":if("/"!=y&&"\\"!=y){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),l="relative path";continue}"\\"==y&&c("\\ is an invalid code point."),l="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=y){c("Expected '/', got: "+y),l="authority ignore slashes";continue}l="authority second slash";break;case"authority second slash":if(l="authority ignore slashes","/"!=y){c("Expected '/', got: "+y);continue}break;case"authority ignore slashes":if("/"!=y&&"\\"!=y){l="authority";continue}c("Expected authority, got: "+y);break;case"authority":if("@"==y){v&&(c("@ already seen."),d+="%40"),v=!0;for(var S=0;S<d.length;S++){var T=d[S];if("\t"!=T&&"\n"!=T&&"\r"!=T)if(":"!=T||null!==this._password){var M=o(T);null!==this._password?this._password+=M:this._username+=M}else this._password="";else c("Invalid whitespace in authority.")}d=""}else{if(f==y||"/"==y||"\\"==y||"?"==y||"#"==y){u-=d.length,d="",l="host";continue}d+=y}break;case"file host":if(f==y||"/"==y||"\\"==y||"?"==y||"#"==y){2!=d.length||!m.test(d[0])||":"!=d[1]&&"|"!=d[1]?0==d.length?l="relative path start":(this._host=r.call(this,d),d="",l="relative path start"):l="relative path";continue}"\t"==y||"\n"==y||"\r"==y?c("Invalid whitespace in file host."):d+=y;break;case"host":case"hostname":if(":"!=y||g){if(f==y||"/"==y||"\\"==y||"?"==y||"#"==y){if(this._host=r.call(this,d),d="",l="relative path start",a)break e;continue}"\t"!=y&&"\n"!=y&&"\r"!=y?("["==y?g=!0:"]"==y&&(g=!1),d+=y):c("Invalid code point in host/hostname: "+y)}else if(this._host=r.call(this,d),d="",l="port","hostname"==a)break e;break;case"port":if(/[0-9]/.test(y))d+=y;else{if(f==y||"/"==y||"\\"==y||"?"==y||"#"==y||a){if(""!=d){var O=parseInt(d,10);O!=p[this._scheme]&&(this._port=O+""),d=""}if(a)break e;l="relative path start";continue}"\t"==y||"\n"==y||"\r"==y?c("Invalid code point in port: "+y):n.call(this)}break;case"relative path start":if("\\"==y&&c("'\\' not allowed in path."),l="relative path","/"!=y&&"\\"!=y)continue;break;case"relative path":if(f!=y&&"/"!=y&&"\\"!=y&&(a||"?"!=y&&"#"!=y))"\t"!=y&&"\n"!=y&&"\r"!=y&&(d+=o(y));else{"\\"==y&&c("\\ not allowed in relative path.");var L;(L=h[d.toLowerCase()])&&(d=L),".."==d?(this._path.pop(),"/"!=y&&"\\"!=y&&this._path.push("")):"."==d&&"/"!=y&&"\\"!=y?this._path.push(""):"."!=d&&("file"==this._scheme&&0==this._path.length&&2==d.length&&m.test(d[0])&&"|"==d[1]&&(d=d[0]+":"),this._path.push(d)),d="","?"==y?(this._query="?",l="query"):"#"==y&&(this._fragment="#",l="fragment")}break;case"query":a||"#"!=y?f!=y&&"\t"!=y&&"\n"!=y&&"\r"!=y&&(this._query+=i(y)):(this._fragment="#",l="fragment");break;case"fragment":f!=y&&"\t"!=y&&"\n"!=y&&"\r"!=y&&(this._fragment+=y)}u++}}function s(){this._scheme="",this._schemeData="",this._username="",this._password=null,this._host="",this._port="",this._path=[],this._query="",this._fragment="",this._isInvalid=!1,this._isRelative=!1}function c(e,t){void 0===t||t instanceof c||(t=new c(String(t))),this._url=e,s.call(this);var n=e.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g,"");a.call(this,n,null,t)}var l=!1;if(!e.forceJURL)try{var u=new URL("b","http://a");u.pathname="c%20d",l="http://a/c%20d"===u.href}catch(d){}if(!l){var p=Object.create(null);p.ftp=21,p.file=0,p.gopher=70,p.http=80,p.https=443,p.ws=80,p.wss=443;var h=Object.create(null);h["%2e"]=".",h[".%2e"]="..",h["%2e."]="..",h["%2e%2e"]="..";var f=void 0,m=/[a-zA-Z]/,w=/[a-zA-Z0-9\+\-\.]/;c.prototype={toString:function(){return this.href},get href(){if(this._isInvalid)return this._url;var e="";return""==this._username&&null==this._password||(e=this._username+(null!=this._password?":"+this._password:"")+"@"),this.protocol+(this._isRelative?"//"+e+this.host:"")+this.pathname+this._query+this._fragment},set href(e){s.call(this),a.call(this,e)},get protocol(){return this._scheme+":"},set protocol(e){this._isInvalid||a.call(this,e+":","scheme start")},get host(){return this._isInvalid?"":this._port?this._host+":"+this._port:this._host},set host(e){!this._isInvalid&&this._isRelative&&a.call(this,e,"host")},get hostname(){return this._host},set hostname(e){!this._isInvalid&&this._isRelative&&a.call(this,e,"hostname")},get port(){return this._port},set port(e){!this._isInvalid&&this._isRelative&&a.call(this,e,"port")},get pathname(){return this._isInvalid?"":this._isRelative?"/"+this._path.join("/"):this._schemeData},set pathname(e){!this._isInvalid&&this._isRelative&&(this._path=[],a.call(this,e,"relative path start"))},get search(){return this._isInvalid||!this._query||"?"==this._query?"":this._query},set search(e){!this._isInvalid&&this._isRelative&&(this._query="?","?"==e[0]&&(e=e.slice(1)),a.call(this,e,"query"))},get hash(){return this._isInvalid||!this._fragment||"#"==this._fragment?"":this._fragment},set hash(e){this._isInvalid||(this._fragment="#","#"==e[0]&&(e=e.slice(1)),a.call(this,e,"fragment"))},get origin(){var e;if(this._isInvalid||!this._scheme)return"";switch(this._scheme){case"data":case"file":case"javascript":case"mailto":return"null"}return e=this.host,e?this._scheme+"://"+e:""}};var v=e.URL;v&&(c.createObjectURL=function(e){return v.createObjectURL.apply(v,arguments)},c.revokeObjectURL=function(e){v.revokeObjectURL(e)}),e.URL=c}}(self),function(e){function t(e){y.push(e),b||(b=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){b=!1;var e=y;y=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(e){e.nodes_.forEach(function(t){var n=w.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var r=w.get(n);if(r)for(var o=0;o<r.length;o++){var i=r[o],a=i.options;if(n===e||a.subtree){var s=t(a);s&&i.enqueue(s)}}}}function a(e){this.callback_=e,this.nodes_=[],this.records_=[],this.uid_=++E}function s(e,t){this.type=e,this.target=t,this.addedNodes=[],this.removedNodes=[],this.previousSibling=null,this.nextSibling=null,this.attributeName=null,this.attributeNamespace=null,this.oldValue=null}function c(e){var t=new s(e.type,e.target);return t.addedNodes=e.addedNodes.slice(),t.removedNodes=e.removedNodes.slice(),t.previousSibling=e.previousSibling,t.nextSibling=e.nextSibling,t.attributeName=e.attributeName,t.attributeNamespace=e.attributeNamespace,t.oldValue=e.oldValue,t}function l(e,t){return _=new s(e,t)}function u(e){return S?S:(S=c(_),S.oldValue=e,S)}function d(){_=S=void 0}function p(e){return e===S||e===_}function h(e,t){return e===t?e:S&&p(e)?S:null}function f(e,t,n){this.observer=e,this.target=t,this.options=n,this.transientObservedNodes=[]}if(!e.JsMutationObserver){var m,w=new WeakMap;if(/Trident|Edge/.test(navigator.userAgent))m=setTimeout;else if(window.setImmediate)m=window.setImmediate;else{var v=[],g=String(Math.random());window.addEventListener("message",function(e){if(e.data===g){var t=v;v=[],t.forEach(function(e){e()})}}),m=function(e){v.push(e),window.postMessage(g,"*")}}var b=!1,y=[],E=0;a.prototype={observe:function(e,t){if(e=n(e),!t.childList&&!t.attributes&&!t.characterData||t.attributeOldValue&&!t.attributes||t.attributeFilter&&t.attributeFilter.length&&!t.attributes||t.characterDataOldValue&&!t.characterData)throw new SyntaxError;var r=w.get(e);r||w.set(e,r=[]);for(var o,i=0;i<r.length;i++)if(r[i].observer===this){o=r[i],o.removeListeners(),o.options=t;break}o||(o=new f(this,e,t),r.push(o),this.nodes_.push(e)),o.addListeners()},disconnect:function(){this.nodes_.forEach(function(e){for(var t=w.get(e),n=0;n<t.length;n++){var r=t[n];if(r.observer===this){r.removeListeners(),t.splice(n,1);break}}},this),this.records_=[]},takeRecords:function(){var e=this.records_;return this.records_=[],e}};var _,S;f.prototype={enqueue:function(e){var n=this.observer.records_,r=n.length;if(n.length>0){var o=n[r-1],i=h(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=w.get(e);t||w.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=w.get(e),n=0;n<t.length;n++)if(t[n]===this){t.splice(n,1);break}},this)},handleEvent:function(e){switch(e.stopImmediatePropagation(),e.type){case"DOMAttrModified":var t=e.attrName,n=e.relatedNode.namespaceURI,r=e.target,o=new l("attributes",r);o.attributeName=t,o.attributeNamespace=n;var a=e.attrChange===MutationEvent.ADDITION?null:e.prevValue;i(r,function(e){if(e.attributes&&(!e.attributeFilter||!e.attributeFilter.length||e.attributeFilter.indexOf(t)!==-1||e.attributeFilter.indexOf(n)!==-1))return e.attributeOldValue?u(a):o});break;case"DOMCharacterDataModified":var r=e.target,o=l("characterData",r),a=e.prevValue;i(r,function(e){if(e.characterData)return e.characterDataOldValue?u(a):o});break;case"DOMNodeRemoved":this.addTransientObserver(e.target);case"DOMNodeInserted":var s,c,p=e.target;"DOMNodeInserted"===e.type?(s=[p],c=[]):(s=[],c=[p]);var h=p.previousSibling,f=p.nextSibling,o=l("childList",e.target.parentNode);o.addedNodes=s,o.removedNodes=c,o.previousSibling=h,o.nextSibling=f,i(e.relatedNode,function(e){if(e.childList)return o})}d()}},e.JsMutationObserver=a,e.MutationObserver||(e.MutationObserver=a,a._isPolyfilled=!0)}}(self),function(e){"use strict";if(!window.performance||!window.performance.now){var t=Date.now();window.performance={now:function(){return Date.now()-t}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}());var n=function(){var e=document.createEvent("Event");return e.initEvent("foo",!0,!0),e.preventDefault(),e.defaultPrevented}();if(!n){var r=Event.prototype.preventDefault;Event.prototype.preventDefault=function(){this.cancelable&&(r.call(this),Object.defineProperty(this,"defaultPrevented",{get:function(){return!0},configurable:!0}))}}var o=/Trident/.test(navigator.userAgent);if((!window.CustomEvent||o&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n},window.CustomEvent.prototype=window.Event.prototype),!window.Event||o&&"function"!=typeof window.Event){var i=window.Event;window.Event=function(e,t){t=t||{};var n=document.createEvent("Event");return n.initEvent(e,Boolean(t.bubbles),Boolean(t.cancelable)),n},window.Event.prototype=i.prototype}}(window.WebComponents),window.HTMLImports=window.HTMLImports||{flags:{}},function(e){function t(e,t){t=t||f,r(function(){i(e,t)},t)}function n(e){return"complete"===e.readyState||e.readyState===v}function r(e,t){if(n(t))e&&e();else{var o=function(){"complete"!==t.readyState&&t.readyState!==v||(t.removeEventListener(g,o),r(e,t))};t.addEventListener(g,o)}}function o(e){e.target.__loaded=!0}function i(e,t){function n(){c==l&&e&&e({allImports:s,loadedImports:u,errorImports:d})}function r(e){o(e),u.push(this),c++,n()}function i(e){ +d.push(this),c++,n()}var s=t.querySelectorAll("link[rel=import]"),c=0,l=s.length,u=[],d=[];if(l)for(var p,h=0;h<l&&(p=s[h]);h++)a(p)?(u.push(this),c++,n()):(p.addEventListener("load",r),p.addEventListener("error",i));else n()}function a(e){return d?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;n<r&&(t=e[n]);n++)c(t)&&l(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function l(e){var t=e["import"];t?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",d=Boolean(u in document.createElement("link")),p=Boolean(window.ShadowDOMPolyfill),h=function(e){return p?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},f=h(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return h(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(f,"_currentScript",m);var w=/Trident/.test(navigator.userAgent),v=w?"complete":"interactive",g="readystatechange";d&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;n<r&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,r=t.length;n<r&&(e=t[n]);n++)l(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=f.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),f.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=d,e.rootDocument=f,e.whenReady=t,e.isIE=w}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,r,o){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}r.call(o,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;n<r&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;s<c&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,r=e.length;n<r&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,r=e.length;n<r&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,l=e.isIE,u=e.IMPORT_LINK_TYPE,d="link[rel="+u+"]",p={documentSelectors:d,importsSelectors:[d,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),l&&"style"===e.localName){var o=!1;if(e.textContent.indexOf("@import")==-1)o=!0;else if(e.sheet){o=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;c<s&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;i<a&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return!t(e)||void 0!==e.__doc}};e.parser=p,e.IMPORT_SELECTOR=d}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(o),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,l=e.Loader,u=e.Observer,d=e.parser,p={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){h.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);h.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}d.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),d.parseNext()},loadedAll:function(){d.parseNext()}},h=new l(p.loaded.bind(p),p.loadedAll.bind(p));if(p.observer=new u,!document.baseURI){var f={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",f),Object.defineProperty(c,"baseURI",f)}e.importer=p,e.importLoader=h}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;s<c&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(r)}var n=e.initializeModules;e.isIE;if(!e.useNative){n();var r=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return!!t(e)||void r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;s<c&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return!!e.upgrade(t,n)||void(n&&a(t))}function r(e,t){b(e,function(e){if(n(e,t))return!0})}function o(e){S.push(e),_||(_=!0,setTimeout(i))}function i(){_=!1;for(var e,t=S,n=0,r=t.length;n<r&&(e=t[n]);n++)e();S=[]}function a(e){E?o(function(){s(e)}):s(e)}function s(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){l(e),b(e,function(e){l(e)})}function l(e){E?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function d(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function p(e){if(e.shadowRoot&&!e.shadowRoot.__watched){g.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function h(e,n){if(g.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=d(e);n.forEach(function(e){"childList"===e.type&&(T(e.addedNodes,function(e){e.localName&&t(e,a)}),T(e.removedNodes,function(e){e.localName&&c(e)}))}),g.dom&&console.groupEnd()}function f(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(h(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(h.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function w(e){e=window.wrap(e),g.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),g.dom&&console.groupEnd()}function v(e){y(e,w)}var g=e.flags,b=e.forSubtree,y=e.forDocumentTree,E=window.MutationObserver._isPolyfilled&&g["throttle-attached"];e.hasPolyfillMutations=E,e.hasThrottledAttached=E;var _=!1,S=[],T=Array.prototype.forEach.call.bind(Array.prototype.forEach),M=Element.prototype.createShadowRoot;M&&(Element.prototype.createShadowRoot=function(){var e=M.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=p,e.upgradeDocumentTree=v,e.upgradeDocument=w,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=f}),window.CustomElements.addModule(function(e){function t(t,r){if("template"===t.localName&&window.HTMLTemplateElement&&HTMLTemplateElement.decorate&&HTMLTemplateElement.decorate(t),!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(o(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(l(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c["extends"]&&(c["extends"]=c["extends"].toLowerCase()),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=d(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&w(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t<E.length;t++)if(e===E[t])return!0}function i(e){var t=l(e);return t?i(t["extends"]).concat([t]):[]}function a(e){for(var t,n=e["extends"],r=0;t=e.ancestry[r];r++)n=t.is&&t.tag;e.tag=n||e.__name,n&&(e.is=e.__name)}function s(e){if(!Object.__proto__){var t=HTMLElement.prototype;if(e.is){var n=document.createElement(e.tag);t=Object.getPrototypeOf(n)}for(var r,o=e.prototype,i=!1;o;)o==t&&(i=!0),r=Object.getPrototypeOf(o),r&&(o.__proto__=r),o=r;i||console.warn(e.tag+" prototype not found in prototype chain for "+e.is),e["native"]=t}}function c(e){return g(T(e.tag),e)}function l(e){if(e)return _[e.toLowerCase()]}function u(e,t){_[e]=t}function d(e){return function(){return c(e)}}function p(e,t,n){return e===S?h(t,n):M(e,t)}function h(e,t){e&&(e=e.toLowerCase()),t&&(t=t.toLowerCase());var n=l(t||e);if(n){if(e==n.tag&&t==n.is)return new n.ctor;if(!t&&!n.is)return new n.ctor}var r;return t?(r=h(e),r.setAttribute("is",t),r):(r=T(e),e.indexOf("-")>=0&&b(r,HTMLElement),r)}function f(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return v(e),e}}var m,w=(e.isIE,e.upgradeDocumentTree),v=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],_={},S="http://www.w3.org/1999/xhtml",T=document.createElement.bind(document),M=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},f(Node.prototype,"cloneNode"),f(document,"importNode"),document.registerElement=t,document.createElement=h,document.createElementNS=p,e.registry=_,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=l,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,r=e.initializeModules;e.isIE;if(n){var o=function(){};e.watchShadow=o,e.upgrade=o,e.upgradeAll=o,e.upgradeDocumentTree=o,e.upgradeSubtree=o,e.takeRecords=o,e["instanceof"]=function(e,t){return e instanceof t}}else r();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){Function.prototype.bind||(Function.prototype.bind=function(e){var t=this,n=Array.prototype.slice.call(arguments,1);return function(){var r=n.slice();return r.push.apply(r,arguments),t.apply(e,r)}})}(window.WebComponents),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents),function(e){window.Platform=e}(window.WebComponents);
\ No newline at end of file diff --git a/traceviewer/src/test/java/io/perfmark/traceviewer/TraceEventViewerTest.java b/traceviewer/src/test/java/io/perfmark/traceviewer/TraceEventViewerTest.java new file mode 100644 index 0000000..fd0094e --- /dev/null +++ b/traceviewer/src/test/java/io/perfmark/traceviewer/TraceEventViewerTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Google LLC + * + * 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 io.perfmark.traceviewer; + +import io.perfmark.PerfMark; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TraceEventViewerTest { + + /** This is an example function to show how to use the recorder. */ + @Test + @Ignore + public void exampleRecorder() throws Exception { + PerfMark.setEnabled(true); + PerfMark.startTask(this, s -> s.getClass().getSimpleName() + ".this"); + PerfMark.stopTask(); + PerfMark.startTask(this, s -> s.getClass().getSimpleName() + ".that"); + PerfMark.stopTask(); + PerfMark.startTask(this, s -> s.getClass().getSimpleName() + "::hi"); + PerfMark.stopTask(); + PerfMark.startTask(this, s -> s.getClass().getSimpleName() + "::hey"); + PerfMark.stopTask(); + PerfMark.startTask(this, s -> s.getClass().getSimpleName() + "::hey"); + PerfMark.stopTask(); + PerfMark.setEnabled(false); + TraceEventViewer.writeTraceHtml(); + } +} diff --git a/tracewriter/BUILD.bazel b/tracewriter/BUILD.bazel new file mode 100644 index 0000000..39e07be --- /dev/null +++ b/tracewriter/BUILD.bazel @@ -0,0 +1,15 @@ +java_library( + name = "tracewriter", + srcs = glob([ + "src/main/java/io/perfmark/tracewriter/*.java", + ]), + visibility = ["//visibility:public"], + deps = [ + "//impl:mark", + "//impl:mark-list", + "//impl:storage", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_code_gson_gson", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/tracewriter/build.gradle.kts b/tracewriter/build.gradle.kts new file mode 100644 index 0000000..930e3aa --- /dev/null +++ b/tracewriter/build.gradle.kts @@ -0,0 +1,27 @@ +buildscript { + extra.apply{ + set("moduleName", "io.perfmark.tracewriter") + } +} + +description = "PerfMark Tracer Output" + +val jdkVersion = JavaVersion.VERSION_1_7 + +dependencies { + api(project(":perfmark-impl")) + // Included because it's easy to forget + runtimeOnly(project(":perfmark-java6")) + + implementation(project(":perfmark-api")) + implementation("com.google.code.gson:gson:2.9.0") + + compileOnly(libs.jsr305) + compileOnly(libs.errorprone) +} + +tasks.getByName<JavaCompile>("compileJava") { + sourceCompatibility = jdkVersion.toString() + targetCompatibility = jdkVersion.toString() + options.compilerArgs.add("-Xlint:-options") +} diff --git a/tracewriter/src/main/java/io/perfmark/tracewriter/MarkListWalker.java b/tracewriter/src/main/java/io/perfmark/tracewriter/MarkListWalker.java new file mode 100644 index 0000000..8456c86 --- /dev/null +++ b/tracewriter/src/main/java/io/perfmark/tracewriter/MarkListWalker.java @@ -0,0 +1,251 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.tracewriter; + +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkList; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +class MarkListWalker { + MarkListWalker() {} + + static final String UNKNOWN_TASK_NAME = "(unknown)"; + + // TODO: make sure the generations dont have any timestamp overlap + final void walk(List<? extends MarkList> markLists, long nowNanoTime) { + Map<Long, List<MarkList>> generationToMarkLists = groupMarkListsByGeneration(markLists); + for (Map.Entry<Long, List<MarkList>> entry : generationToMarkLists.entrySet()) { + enterGeneration(entry.getKey()); + for (MarkList markList : entry.getValue()) { + enterMarkList(markList.getThreadName(), markList.getThreadId(), markList.getMarkListId()); + Deque<Mark> fakeStarts = new ArrayDeque<>(); + Deque<Mark> fakeEnds = new ArrayDeque<>(); + Set<Mark> unmatchedPairMarks = + Collections.newSetFromMap(new IdentityHashMap<Mark, Boolean>()); + createFakes(fakeStarts, fakeEnds, unmatchedPairMarks, markList, nowNanoTime); + for (Mark mark : fakeStarts) { + onTaskStart(mark, true, false); + } + for (Mark mark : markList) { + onRealMark(mark, unmatchedPairMarks); + } + for (Mark mark : fakeEnds) { + onTaskEnd(mark, false, true); + } + exitMarkList(); + } + exitGeneration(); + } + } + + protected void enterGeneration(long generation) {} + + protected void exitGeneration() {} + + protected void enterMarkList(String threadName, long threadId, long markListId) {} + + protected void exitMarkList() {} + + private void onRealMark(Mark mark, Collection<Mark> unmatchedPairMarks) { + switch (mark.getOperation().getOpType()) { + case TASK_START: + onTaskStart(mark, false, unmatchedPairMarks.contains(mark)); + return; + case TASK_END: + onTaskEnd(mark, unmatchedPairMarks.contains(mark), false); + return; + case TAG: + onAttachTag(mark); + return; + case EVENT: + onEvent(mark); + return; + case LINK: + onLink(mark); + return; + case NONE: + break; + } + throw new AssertionError(); + } + + protected void onTaskStart(Mark mark, boolean unmatchedStart, boolean unmatchedEnd) {} + + protected void onTaskEnd(Mark mark, boolean unmatchedStart, boolean unmatchedEnd) {} + + protected void onLink(Mark mark) {} + + protected void onEvent(Mark mark) {} + + protected void onAttachTag(Mark mark) {} + + private static Map<Long, List<MarkList>> groupMarkListsByGeneration( + List<? extends MarkList> markLists) { + Map<Long, List<MarkList>> generationToMarkLists = new TreeMap<>(); + for (MarkList markList : markLists) { + if (markList.isEmpty()) { + continue; + } + Map<Long, List<Mark>> generationToMarks = new TreeMap<>(); + for (Mark mark : markList) { + List<Mark> groupedMarks = generationToMarks.get(mark.getGeneration()); + if (groupedMarks == null) { + generationToMarks.put(mark.getGeneration(), groupedMarks = new ArrayList<>()); + } + groupedMarks.add(mark); + } + // note: marklists without any marks are lost here, since they have no generation. + for (Map.Entry<Long, List<Mark>> entry : generationToMarks.entrySet()) { + List<MarkList> groupedMarkLists = generationToMarkLists.get(entry.getKey()); + if (groupedMarkLists == null) { + generationToMarkLists.put(entry.getKey(), groupedMarkLists = new ArrayList<>()); + } + groupedMarkLists.add(markList.toBuilder().setMarks(entry.getValue()).build()); + } + } + // TODO: make a defensive copy of this and the sublists + return generationToMarkLists; + } + + private static void createFakes( + Deque<? super Mark> fakeStarts, + Deque<? super Mark> fakeEnds, + Set<? super Mark> unmatchedPairMarks, + List<Mark> marks, + long nowNanoTime) { + final Deque<Mark> unmatchedMarks = new ArrayDeque<>(); + long[] nanoTimeBounds = new long[2]; // first, last + nanoTimeBounds[0] = nowNanoTime; // forces each subsequent overwrite to succeed. + nanoTimeBounds[1] = nowNanoTime; + + loop: + for (Mark mark : marks) { + setNanoTimeBounds(nanoTimeBounds, mark); + switch (mark.getOperation().getOpType()) { + case TASK_START: + unmatchedMarks.addLast(mark); + continue loop; + case TASK_END: + if (!unmatchedMarks.isEmpty()) { + // TODO: maybe double check the tags and task names match + unmatchedMarks.removeLast(); + } else { + fakeStarts.addFirst(createFakeStart(mark, nanoTimeBounds[0])); + unmatchedPairMarks.add(mark); + } + continue loop; + case EVENT: + case LINK: + case TAG: + continue loop; + case NONE: + break; + } + throw new AssertionError(); + } + for (Mark unmatchedMark : unmatchedMarks) { + fakeEnds.addFirst(createFakeEnd(unmatchedMark, nanoTimeBounds[1])); + unmatchedPairMarks.add(unmatchedMark); + } + unmatchedMarks.clear(); + } + + private static void setNanoTimeBounds(long[] nanoTimeBounds, Mark mark) { + switch (mark.getOperation().getOpType()) { + case TASK_START: + case TASK_END: + case EVENT: + if (mark.getNanoTime() - nanoTimeBounds[0] < 0) { + nanoTimeBounds[0] = mark.getNanoTime(); + } + if (mark.getNanoTime() - nanoTimeBounds[1] > 0) { + nanoTimeBounds[1] = mark.getNanoTime(); + } + return; + case LINK: + case TAG: + return; + case NONE: + break; + } + throw new AssertionError(); + } + + private static Mark createFakeEnd(Mark start, long lastNanoTime) { + switch (start.getOperation()) { + case TASK_START_N1S1: + return Mark.taskEnd(start.getGeneration(), lastNanoTime, start.getTaskName()); + case TASK_START_N1S2: + return Mark.taskEnd( + start.getGeneration(), lastNanoTime, start.getTaskName(), start.getSubTaskName()); + case TASK_END_N1S0: + case TASK_END_N1S1: + case TASK_END_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case EVENT_N2S2: + case EVENT_N2S3: + case LINK: + case TAG_N0S1: + case TAG_KEYED_N0S2: + case TAG_KEYED_N2S1: + case TAG_KEYED_N1S1: + case TAG_N1S0: + case TAG_N1S1: + case NONE: + break; + } + throw new AssertionError(start.getOperation()); + } + + private static Mark createFakeStart(Mark end, long firstNanoTime) { + switch (end.getOperation()) { + case TASK_END_N1S0: + return Mark.taskStart(end.getGeneration(), firstNanoTime, UNKNOWN_TASK_NAME); + case TASK_END_N1S1: + return Mark.taskStart(end.getGeneration(), firstNanoTime, end.getTaskName()); + case TASK_END_N1S2: + return Mark.taskStart( + end.getGeneration(), firstNanoTime, end.getTaskName(), end.getSubTaskName()); + case NONE: + case TASK_START_N1S1: + case TASK_START_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case EVENT_N2S2: + case EVENT_N2S3: + case LINK: + case TAG_N0S1: + case TAG_KEYED_N0S2: + case TAG_KEYED_N2S1: + case TAG_KEYED_N1S1: + case TAG_N1S0: + case TAG_N1S1: + break; + } + throw new AssertionError(end.getOperation()); + } +} diff --git a/tracewriter/src/main/java/io/perfmark/tracewriter/TraceEvent.java b/tracewriter/src/main/java/io/perfmark/tracewriter/TraceEvent.java new file mode 100644 index 0000000..08e8ca5 --- /dev/null +++ b/tracewriter/src/main/java/io/perfmark/tracewriter/TraceEvent.java @@ -0,0 +1,267 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.tracewriter; + +import com.google.gson.annotations.SerializedName; +import io.perfmark.impl.Mark; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckReturnValue; +import javax.annotation.Nullable; + +@CheckReturnValue +final class TraceEvent implements Cloneable { + + private TraceEvent() {} + + static final TraceEvent EVENT = new TraceEvent(); + + @SerializedName("ph") + @SuppressWarnings("unused") + private String phase; + + @SerializedName("name") + @SuppressWarnings("unused") + private String name; + + @Nullable + @SerializedName("cat") + @SuppressWarnings("unused") + private String categories; + + @Nullable + @SerializedName("ts") + @SuppressWarnings("unused") + private Double traceClockMicros; + + @Nullable + @SerializedName("pid") + @SuppressWarnings("unused") + private Long pid; + + @SerializedName("tid") + @Nullable + @SuppressWarnings("unused") + private Long tid; + + @Nullable + @SerializedName("id") + @SuppressWarnings("unused") + private Long id; + + @Nullable + @SerializedName("args") + @SuppressWarnings("unused") + private TagMap args = null; + + @Nullable + @SerializedName("cname") + @SuppressWarnings("unused") + private String colorName = null; + + TraceEvent name(String name) { + if (name == null) { + throw new NullPointerException("name"); + } + TraceEvent other = clone(); + other.name = name; + return other; + } + + TraceEvent categories(String... categories) { + if (categories == null) { + throw new NullPointerException("categories"); + } + return categories(Arrays.asList(categories)); + } + + TraceEvent categories(List<String> categories) { + if (categories == null) { + throw new NullPointerException("categories"); + } + TraceEvent other = clone(); + if (!categories.isEmpty()) { + StringBuilder sb = new StringBuilder(); + ListIterator<String> it = categories.listIterator(); + sb.append(it.next()); + while (it.hasNext()) { + String next = it.next(); + if (next == null) { + throw new NullPointerException("next null at " + (it.nextIndex() - 1)); + } + sb.append(',').append(next); + } + other.categories = sb.toString(); + } else { + other.categories = null; + } + return other; + } + + strictfp TraceEvent traceClockNanos(long traceClockNanos) { + TraceEvent other = clone(); + other.traceClockMicros = traceClockNanos / 1000.0; + return other; + } + + TraceEvent phase(String phase) { + if (phase == null) { + throw new NullPointerException("phase"); + } + TraceEvent other = clone(); + other.phase = phase; + return other; + } + + TraceEvent tid(long tid) { + TraceEvent other = clone(); + other.tid = tid; + return other; + } + + TraceEvent pid(long pid) { + TraceEvent other = clone(); + other.pid = pid; + return other; + } + + TraceEvent id(long id) { + TraceEvent other = clone(); + other.id = id; + return other; + } + + /** + * Note This should only be used for tags, as the map size is used to determine the arg names in + * TraceEventWriter. This will overwrite any existing args. + * + * @param tagMap the args to use. + * @return this + */ + TraceEvent args(TagMap tagMap) { + if (tagMap == null) { + throw new NullPointerException("tagMap"); + } + TraceEvent other = clone(); + other.args = tagMap; + return other; + } + + TagMap args() { + if (args == null) { + return TagMap.EMPTY; + } else { + return args; + } + } + + @Override + protected TraceEvent clone() { + try { + return (TraceEvent) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + static final class TagMap extends AbstractMap<String, Object> { + + static final TagMap EMPTY = + new TagMap(Collections.<Entry<String, ?>>emptyList(), Collections.emptyList()); + + private final List<Entry<String, ?>> keyedValues; + private final List<?> unkeyedValues; + + private TagMap(List<Entry<String, ?>> keyedValues, List<?> unkeyedValues) { + this.keyedValues = keyedValues; + this.unkeyedValues = unkeyedValues; + } + + TagMap withUnkeyed(@Nullable String tagName, long tagId) { + List<Object> unkeyedValues = null; + if (tagName != null && !Mark.NO_TAG_NAME.equals(tagName)) { + unkeyedValues = new ArrayList<>(this.unkeyedValues); + unkeyedValues.add(tagName); + } + if (tagId != Mark.NO_TAG_ID) { + unkeyedValues = unkeyedValues != null ? unkeyedValues : new ArrayList<>(this.unkeyedValues); + unkeyedValues.add(tagId); + } + if (unkeyedValues != null) { + return new TagMap(keyedValues, Collections.unmodifiableList(unkeyedValues)); + } else { + return new TagMap(keyedValues, this.unkeyedValues); + } + } + + TagMap withKeyed(@Nullable String tagName, Object tagValue) { + List<Entry<String, ?>> keyedValues = new ArrayList<>(this.keyedValues); + keyedValues.add(new SimpleImmutableEntry<>(String.valueOf(tagName), tagValue)); + return new TagMap(Collections.unmodifiableList(keyedValues), unkeyedValues); + } + + TagMap withKeyed(@Nullable String tagName, long tagValue0, long tagValue1) { + List<Entry<String, ?>> keyedValues = new ArrayList<>(this.keyedValues); + keyedValues.add( + new SimpleImmutableEntry<>(String.valueOf(tagName), tagValue0 + ":" + tagValue1)); + return new TagMap(Collections.unmodifiableList(keyedValues), unkeyedValues); + } + + @Override + public Set<Entry<String, Object>> entrySet() { + List<Entry<String, ?>> pairs = new ArrayList<>(keyedValues.size() + unkeyedValues.size()); + pairs.addAll(keyedValues); + for (Object value : unkeyedValues) { + if (value instanceof Long) { + pairs.add(new SimpleImmutableEntry<>("id", value)); + } else if (value instanceof String) { + pairs.add(new SimpleImmutableEntry<>("tag", value)); + } else { + pairs.add(new SimpleImmutableEntry<>("tag", String.valueOf(value))); + } + } + + Map<String, Object> ret = new LinkedHashMap<>(); + addEntry: + for (Entry<String, ?> kv : pairs) { + String name = kv.getKey(); + Object value = kv.getValue(); + String derivedName = name; + int usages = 0; + while (true) { + if (!ret.containsKey(derivedName)) { + ret.put(derivedName, value); + continue addEntry; + } + if (ret.get(derivedName).equals(value)) { + continue addEntry; + } + usages++; + derivedName = name + " (" + usages + ')'; + } + } + return Collections.unmodifiableSet(ret.entrySet()); + } + } +} diff --git a/tracewriter/src/main/java/io/perfmark/tracewriter/TraceEventWriter.java b/tracewriter/src/main/java/io/perfmark/tracewriter/TraceEventWriter.java new file mode 100644 index 0000000..d9a1fc3 --- /dev/null +++ b/tracewriter/src/main/java/io/perfmark/tracewriter/TraceEventWriter.java @@ -0,0 +1,539 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.tracewriter; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.annotations.SerializedName; +import io.perfmark.impl.Mark; +import io.perfmark.impl.MarkList; +import io.perfmark.impl.Storage; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPOutputStream; + +/** + * Writes the PerfMark results to a "Trace Event" JSON file usable by the Chromium Profiler + * "Catapult". The format is defined at + * https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview + * + * <p>This code is <strong>NOT</strong> API stable, and may be removed in the future, or changed + * without notice. + * + * @since 0.16.0 + */ +public final class TraceEventWriter { + + private static final Logger logger = Logger.getLogger(TraceEventWriter.class.getName()); + + /** + * Writes trace events the home directory. By default, it prefers the location in {@code + * $XDG_DATA_HOME/perfmark} environment variable. If unset, it attempts to use {@code + * $HOME/.local/share/perfmark}. + * + * <p>Authors note: if you are on Windows, or the above defaults aren't right, I'm not really sure + * where else is a good place to put this data. Please file an issue at https://www.perfmark.io/ + * if you have a preference. + * + * <p>Updated in 0.17.0 to return the created path. + * + * @throws IOException if there is an error writing to the file. + * @return the path used to create the trace file. + */ + @CanIgnoreReturnValue + public static Path writeTraceEvents() throws IOException { + Path p = pickNextDest(guessDirectory()); + try (OutputStream os = Files.newOutputStream(p); + OutputStream gzos = new GZIPOutputStream(os); + Writer osw = new OutputStreamWriter(gzos, UTF_8)) { + writeTraceEvents(osw); + } + logger.info("Wrote trace to " + p); + return p; + } + + /** + * Writes all trace events in in JSON format to the given destination. + * + * @param destination the destination for the JSON data. + * @throws IOException if there are errors build the JSON, or can't write to the destination. + */ + public static void writeTraceEvents(Writer destination) throws IOException { + writeTraceEvents( + destination, Storage.read(), Storage.getInitNanoTime(), System.nanoTime(), getPid()); + } + + /** + * Writes the trace events gathered from {@link Storage#read()}. This method is not API stable. It + * will be eventually. + * + * @param destination the destination for the JSON data. + * @param markLists the data to use to build the trace event JSON + * @param initNanoTime the time PerfMark classes were first loaded as specified by {@link + * System#nanoTime()} + * @param nowNanoTime the current time as specified by {@link System#nanoTime()}. + * @param pid the PID of the current process. + * @throws IOException if there are errors build the JSON, or can't write to the destination. + */ + public static void writeTraceEvents( + Writer destination, + List<? extends MarkList> markLists, + long initNanoTime, + long nowNanoTime, + long pid) + throws IOException { + List<TraceEvent> traceEvents = new ArrayList<>(); + new TraceEventWalker(traceEvents, pid, initNanoTime).walk(markLists, nowNanoTime); + Gson gson = new GsonBuilder().create(); + try { + gson.toJson(new TraceEventObject(traceEvents), destination); + } catch (JsonIOException e) { + throw new IOException(e); + } + } + + private static Path pickNextDest(Path dir) throws IOException { + String fmt = "perfmark-trace-%03d.json.gz"; + int lo; + int hi = 0; + while (true) { + Path candidate = dir.resolve(String.format(fmt, hi)); + if (!Files.exists(candidate)) { + lo = hi >>> 1; + break; + } + if (hi == 0) { + hi++; + } else if (hi >>> 1 >= Integer.MAX_VALUE) { + throw new IOException("too many files in dir"); + } else { + hi <<= 1; + } + } + // After this point, hi must always point to a non-existent file. + while (hi > lo) { + int mid = (hi + lo) >>> 1; // take THAT, overflow! + Path candidate = dir.resolve(String.format(fmt, mid)); + if (Files.exists(candidate)) { + lo = mid + 1; + } else { + hi = mid; + } + } + return dir.resolve(String.format(fmt, hi)); + } + + private static Path guessDirectory() throws IOException { + final String PERFMARK_TRACE_DIR = "perfmark"; + final String sep = File.separator; + + List<Path> dataHomeChoices = new ArrayList<>(); + String dataHome = System.getenv("XDG_DATA_HOME"); + if (dataHome != null) { + dataHomeChoices.add(new File(dataHome + sep + PERFMARK_TRACE_DIR).toPath()); + } + String home = System.getProperty("user.home"); + if (home != null) { + dataHomeChoices.add( + new File(home + sep + ".local" + sep + "share" + sep + PERFMARK_TRACE_DIR).toPath()); + } + for (Path path : dataHomeChoices) { + if (!Files.exists(path)) { + Files.createDirectories(path); + } else { + if (!Files.isDirectory(path)) { + continue; + } + } + return path; + } + return new File(".").toPath(); + } + + static final class TraceEventObject { + @SerializedName("traceEvents") + @SuppressWarnings("unused") + final List<TraceEvent> traceEvents; + + @SerializedName("displayTimeUnit") + @SuppressWarnings("unused") + final String displayTimeUnit = "ns"; + + @SerializedName("systemTraceEvents") + @SuppressWarnings("unused") + final String systemTraceData = ""; + + @SerializedName("samples") + @SuppressWarnings("unused") + final List<Object> samples = new ArrayList<>(); + + @SerializedName("stackFrames") + @SuppressWarnings("unused") + final Map<String, ?> stackFrames = new HashMap<>(); + + TraceEventObject(List<TraceEvent> traceEvents) { + this.traceEvents = Collections.unmodifiableList(new ArrayList<>(traceEvents)); + } + } + + private static long getPid() { + List<Throwable> errors = new ArrayList<>(0); + Level level = Level.FINE; + try { + try { + Class<?> clz = Class.forName("java.lang.ProcessHandle"); + Method currentMethod = clz.getMethod("current"); + Object processHandle = currentMethod.invoke(null); + Method pidMethod = clz.getMethod("pid"); + return (long) pidMethod.invoke(processHandle); + } catch (Exception | Error e) { + errors.add(e); + } + try { + String name = ManagementFactory.getRuntimeMXBean().getName(); + int index = name.indexOf('@'); + if (index != -1) { + return Long.parseLong(name.substring(0, index)); + } + } catch (Exception | Error e) { + errors.add(e); + } + level = Level.WARNING; + } finally { + for (Throwable error : errors) { + logger.log(level, "Error getting pid", error); + } + } + return -1; + } + + private static final class TraceEventWalker extends MarkListWalker { + + private static final class TaskStart { + final Mark mark; + final int traceEventIdx; + + TaskStart(Mark mark, int traceEventIdx) { + this.mark = mark; + this.traceEventIdx = traceEventIdx; + } + } + + private long uniqueLinkPairId = 1; + private long currentThreadId = -1; + private long currentMarkListId = -1; + private final Deque<TaskStart> taskStack = new ArrayDeque<>(); + private final Map<Long, LinkTuple> linkIdToLinkOut = new LinkedHashMap<>(); + private final List<LinkTuple> linkIdToLinkIn = new ArrayList<>(); + + private final long pid; + private final long initNanoTime; + private final List<TraceEvent> traceEvents; + + TraceEventWalker(List<TraceEvent> traceEvents, long pid, long initNanoTime) { + this.pid = pid; + this.initNanoTime = initNanoTime; + this.traceEvents = traceEvents; + } + + @Override + protected void enterGeneration(long generation) { + linkIdToLinkOut.clear(); + linkIdToLinkIn.clear(); + } + + @Override + protected void exitGeneration() { + for (LinkTuple linkIn : linkIdToLinkIn) { + long inLinkId = linkIn.link.getLinkId(); + long outLinkId = -inLinkId; + LinkTuple linkOut = linkIdToLinkOut.get(outLinkId); + if (linkOut == null) { + // TODO: log? + continue; + } + if (linkOut.markListId == linkIn.markListId) { + // continue; + } + // The name must be the same to match links together. + String name = + "link(" + + taskName(linkOut.lastTaskStart) + + " -> " + + taskName(linkIn.lastTaskStart) + + ")"; + long localUniqueLinkPairId = uniqueLinkPairId++; + traceEvents.add( + TraceEvent.EVENT + .name(name) + .tid(linkOut.threadId) + .pid(pid) + .phase("s") + .id(localUniqueLinkPairId) + .args(TraceEvent.TagMap.EMPTY.withKeyed("linkid", linkOut.link.getLinkId())) + .traceClockNanos(linkOut.lastTaskStart.getNanoTime() - initNanoTime)); + + traceEvents.add( + TraceEvent.EVENT + .name(name) + .tid(linkIn.threadId) + .pid(pid) + .phase("t") + .id(localUniqueLinkPairId) + .args(TraceEvent.TagMap.EMPTY.withKeyed("linkid", linkOut.link.getLinkId())) + .traceClockNanos(linkIn.lastTaskStart.getNanoTime() - initNanoTime)); + } + super.exitGeneration(); + } + + @Override + protected void enterMarkList(String threadName, long threadId, long markListId) { + currentThreadId = threadId; + currentMarkListId = markListId; + traceEvents.add( + TraceEvent.EVENT + .name("thread_name") + .phase("M") + .pid(pid) + .args( + TraceEvent.TagMap.EMPTY + .withKeyed("name", threadName) + .withKeyed("markListId", markListId)) + .tid(currentThreadId)); + } + + @Override + protected void onTaskStart(Mark mark, boolean unmatchedStart, boolean unmatchedEnd) { + assert !(unmatchedStart && unmatchedEnd); + List<String> categories = Collections.emptyList(); + if (unmatchedStart) { + categories = Collections.singletonList("unknownStart"); + } else if (unmatchedEnd) { + categories = Collections.singletonList("unfinished"); + } + TraceEvent traceEvent = + TraceEvent.EVENT + .name(taskName(mark)) + .phase("B") + .pid(pid) + .categories(categories) + .tid(currentThreadId) + .traceClockNanos(mark.getNanoTime() - initNanoTime); + traceEvents.add(traceEvent); + taskStack.add(new TaskStart(mark, traceEvents.size() - 1)); + } + + @Override + @SuppressWarnings("ReferenceEquality") // For checking if it's an empty end mark + protected void onTaskEnd(Mark mark, boolean unmatchedStart, boolean unmatchedEnd) { + assert !(unmatchedStart && unmatchedEnd); + List<String> categories = Collections.emptyList(); + if (unmatchedStart) { + categories = Collections.singletonList("unknownStart"); + } else if (unmatchedEnd) { + categories = Collections.singletonList("unfinished"); + } + // TODO: maybe copy the args from the start task + taskStack.pollLast(); + TraceEvent traceEvent = + TraceEvent.EVENT + .phase("E") + .pid(pid) + .categories(categories) + .tid(currentThreadId) + .traceClockNanos(mark.getNanoTime() - initNanoTime); + String name = taskName(mark); + if (name != MarkListWalker.UNKNOWN_TASK_NAME) { + traceEvent = traceEvent.name(name); + } + traceEvents.add(traceEvent); + } + + @Override + protected void onAttachTag(Mark mark) { + if (taskStack.isEmpty()) { + // In a mark list of only links (i.e. no starts or ends) it's possible there are no tasks + // to bind to. This is probably due to not calling link() correctly. + logger.fine("Tag not associated with any task"); + return; + } + TaskStart taskStart = taskStack.peekLast(); + TraceEvent taskEvent = traceEvents.get(taskStart.traceEventIdx); + TraceEvent.TagMap args = taskEvent.args(); + out: + { + switch (mark.getOperation()) { + case TAG_N0S1: + args = args.withUnkeyed(mark.getTagStringValue(), Mark.NO_TAG_ID); + break out; + case TAG_N1S0: + args = args.withUnkeyed(Mark.NO_TAG_NAME, mark.getTagFirstNumeric()); + break out; + case TAG_N1S1: + args = args.withUnkeyed(mark.getTagStringValue(), mark.getTagFirstNumeric()); + break out; + case TAG_KEYED_N0S2: + args = args.withKeyed(mark.getTagKey(), mark.getTagStringValue()); + break out; + case TAG_KEYED_N1S1: + args = args.withKeyed(mark.getTagKey(), mark.getTagFirstNumeric()); + break out; + case TAG_KEYED_N2S1: + args = + args.withKeyed( + mark.getTagKey(), mark.getTagFirstNumeric(), mark.getTagSecondNumeric()); + break out; + case NONE: + case TASK_START_N1S1: + case TASK_START_N1S2: + case TASK_END_N1S0: + case TASK_END_N1S1: + case TASK_END_N1S2: + case EVENT_N1S1: + case EVENT_N1S2: + case EVENT_N2S2: + case EVENT_N2S3: + case LINK: + break; + } + throw new AssertionError(mark.getOperation()); + } + traceEvents.set(taskStart.traceEventIdx, taskEvent.args(args)); + } + + @Override + protected void onEvent(Mark mark) { + TraceEvent.TagMap tagMap = TraceEvent.TagMap.EMPTY; + out: + { + switch (mark.getOperation()) { + case EVENT_N1S1: + case EVENT_N1S2: + break out; + case EVENT_N2S2: + case EVENT_N2S3: + tagMap = tagMap.withUnkeyed(mark.getTagStringValue(), mark.getTagFirstNumeric()); + break out; + case NONE: + case TASK_START_N1S1: + case TASK_START_N1S2: + case TASK_END_N1S0: + case TASK_END_N1S1: + case TASK_END_N1S2: + case LINK: + case TAG_N0S1: + case TAG_N1S0: + case TAG_N1S1: + case TAG_KEYED_N0S2: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + break; + } + throw new AssertionError(mark.getOperation()); + } + TraceEvent traceEvent = + TraceEvent.EVENT + .name(taskName(mark)) + .phase("i") + .pid(pid) + .args(tagMap) + .tid(currentThreadId) + .traceClockNanos(mark.getNanoTime() - initNanoTime); + traceEvents.add(traceEvent); + } + + static final class LinkTuple { + final Mark lastTaskStart; + final Mark link; + final long threadId; + final long markListId; + + LinkTuple(Mark lastTaskStart, Mark link, long threadId, long markListId) { + this.lastTaskStart = lastTaskStart; + this.link = link; + this.threadId = threadId; + this.markListId = markListId; + } + } + + @Override + protected void onLink(Mark mark) { + if (taskStack.isEmpty()) { + // In a mark list of only links (i.e. no starts or ends) it's possible there are no tasks + // to bind to. This is probably due to not calling link() correctly. + logger.fine("Link not associated with any task"); + return; + } + LinkTuple linkTuple = + new LinkTuple(taskStack.peekLast().mark, mark, currentThreadId, currentMarkListId); + if (mark.getLinkId() > 0) { + LinkTuple old = linkIdToLinkOut.put(mark.getLinkId(), linkTuple); + assert old == null; + } else if (mark.getLinkId() < 0) { + linkIdToLinkIn.add(linkTuple); + } + } + } + + private static String taskName(Mark mark) { + switch (mark.getOperation()) { + case TASK_END_N1S0: + return MarkListWalker.UNKNOWN_TASK_NAME; + case TASK_START_N1S1: + case TASK_END_N1S1: + case EVENT_N1S1: + case EVENT_N2S2: + return mark.getTaskName(); + case TASK_START_N1S2: + case TASK_END_N1S2: + case EVENT_N1S2: + case EVENT_N2S3: + return mark.getTaskName() + '.' + mark.getSubTaskName(); + case LINK: + case TAG_N0S1: + case TAG_KEYED_N0S2: + case TAG_KEYED_N1S1: + case TAG_KEYED_N2S1: + case TAG_N1S0: + case TAG_N1S1: + case NONE: + throw new UnsupportedOperationException(mark.toString()); + } + throw new AssertionError(mark.getOperation()); + } +} diff --git a/tracewriter/src/main/java/io/perfmark/tracewriter/package-info.java b/tracewriter/src/main/java/io/perfmark/tracewriter/package-info.java new file mode 100644 index 0000000..38443b8 --- /dev/null +++ b/tracewriter/src/main/java/io/perfmark/tracewriter/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +/** + * The Trace Writer package reads the PerfMark recorded tasks, and converts them into the + * Chrome Trace Viewer format. + */ +@javax.annotation.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +package io.perfmark.tracewriter; |