First draft of Snippet sample project
|
@ -0,0 +1,19 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
|
||||
/app/build
|
||||
/.idea/
|
|
@ -0,0 +1,91 @@
|
|||
/build
|
||||
/app/build
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
.idea/jarRepositories.xml
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
|
@ -0,0 +1,40 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.microsoft.sample"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 31
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
||||
implementation project(':snippet')
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,26 @@
|
|||
package com.microsoft.sample;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
assertEquals("com.microsoft.sample", appContext.getPackageName());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.microsoft.sample">
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:name=".SampleApplication"
|
||||
android:theme="@style/Theme.Sample">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,17 @@
|
|||
package com.microsoft.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.microsoft.snippet.Snippet;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
Snippet.find("app_start").endCapture();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.microsoft.sample;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.microsoft.snippet.Snippet;
|
||||
|
||||
public class SampleApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
if(BuildConfig.DEBUG) {
|
||||
Snippet.install(new Snippet.MeasuredExecutionPath());
|
||||
Snippet.newFilter("SampleFilter");
|
||||
Snippet.addFlag(Snippet.FLAG_METADATA_LINE | Snippet.FLAG_METADATA_THREAD_INFO);
|
||||
}
|
||||
Snippet.startCapture("app_start");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
После Ширина: | Высота: | Размер: 3.5 KiB |
После Ширина: | Высота: | Размер: 5.2 KiB |
После Ширина: | Высота: | Размер: 2.6 KiB |
После Ширина: | Высота: | Размер: 3.3 KiB |
После Ширина: | Высота: | Размер: 4.8 KiB |
После Ширина: | Высота: | Размер: 7.3 KiB |
После Ширина: | Высота: | Размер: 7.7 KiB |
После Ширина: | Высота: | Размер: 12 KiB |
После Ширина: | Высота: | Размер: 10 KiB |
После Ширина: | Высота: | Размер: 16 KiB |
|
@ -0,0 +1,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Sample" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Sample</string>
|
||||
</resources>
|
|
@ -0,0 +1,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Sample" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
|
@ -0,0 +1,17 @@
|
|||
package com.microsoft.sample;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:4.2.2"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter() // Warning: this repository is going to shut down soon
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
|
@ -0,0 +1,6 @@
|
|||
#Wed Sep 08 23:02:35 IST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
|
@ -0,0 +1,84 @@
|
|||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
|
@ -0,0 +1,3 @@
|
|||
rootProject.name = "Sample"
|
||||
include ':app'
|
||||
include ':snippet'
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,30 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 31
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree (dir: "libs", include: ["*.jar"])
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,4 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.microsoft.snippet">
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,53 @@
|
|||
package com.microsoft.snippet;
|
||||
|
||||
/**
|
||||
* Wraps all the information that can be returned through the library.
|
||||
* If any other information is needed it can be added ad-hoc.
|
||||
*/
|
||||
public class ExecutionContext {
|
||||
private String mClass;
|
||||
private String mMethod;
|
||||
private int mLineNo;
|
||||
private String mThreadName;
|
||||
private long mExecutionDuration;
|
||||
|
||||
void setClassName(String clazz) {
|
||||
this.mClass = clazz;
|
||||
}
|
||||
|
||||
void setMethod(String method) {
|
||||
this.mMethod = method;
|
||||
}
|
||||
|
||||
void setLineNo(int line) {
|
||||
this.mLineNo = line;
|
||||
}
|
||||
|
||||
void setThreadName(String threadName) {
|
||||
this.mThreadName = threadName;
|
||||
}
|
||||
|
||||
void setExecutionDuration(long duration) {
|
||||
this.mExecutionDuration = duration;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return this.mClass;
|
||||
}
|
||||
|
||||
public String getMethodName() {
|
||||
return this.mMethod;
|
||||
}
|
||||
|
||||
public String getThreadName() {
|
||||
return this.mThreadName;
|
||||
}
|
||||
|
||||
public int getLineNo() {
|
||||
return this.mLineNo;
|
||||
}
|
||||
|
||||
public long getExecutionDuration() {
|
||||
return this.mExecutionDuration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.microsoft.snippet.token.ILogToken;
|
||||
|
||||
/**
|
||||
* Execution path determines how core the functionality of this library should behave.
|
||||
* It might be possible that we do not want to execute the code entirely in release builds or
|
||||
* may want to add some extra information into the existing information and add it to files.
|
||||
* We can plugin a custom execution path or method through this.
|
||||
*/
|
||||
public interface ExecutionPath {
|
||||
|
||||
/**
|
||||
* Return a pair of delta: the time taken to execute the closure. And the standard log message
|
||||
* printed by the Snippet.
|
||||
*
|
||||
* @param message Custom message to print on the log if any.
|
||||
* @param closure Closure who execution duration needs to be measured.
|
||||
* @return Pair of Delta and Log String
|
||||
*/
|
||||
@NonNull
|
||||
ExecutionContext capture(String message, Snippet.Closure closure);
|
||||
|
||||
/**
|
||||
* Return a pair of delta: the time taken to execute the closure. And the standard log message
|
||||
* printed by the Snippet.
|
||||
*
|
||||
* @param closure Closure who execution duration needs to be measured.
|
||||
* @return Pair of Delta and Log String
|
||||
*/
|
||||
@NonNull
|
||||
ExecutionContext capture(Snippet.Closure closure);
|
||||
|
||||
/**
|
||||
* Returns a log token and starts the measurement at this point. The token returned has a
|
||||
* method endCapture() which ends the measurement.
|
||||
*
|
||||
* @return IToken
|
||||
*/
|
||||
ILogToken startCapture();
|
||||
|
||||
/**
|
||||
* Gets a log token and ties it with a tag that can be used to search the instance of log token
|
||||
* using {@link Snippet#find(String)}
|
||||
*
|
||||
* @param tag Unique tag for the log token. If 2 token try to use same tag, the first one to get
|
||||
* the tag wins and other calls becomes no-op.
|
||||
* @return IToken
|
||||
*/
|
||||
ILogToken startCapture(String tag);
|
||||
|
||||
/**
|
||||
* Finds a log token with a tag which has been created previously. Using
|
||||
* {@link Snippet#startCapture(String)}
|
||||
*
|
||||
* @param tag Custom tag. This tag will be used to search the log token across the app.
|
||||
* @return IToken if any available attached with this tag
|
||||
*/
|
||||
ILogToken find(String tag);
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
/**
|
||||
* Provides a class that can be used for capturing variables in an anonymous class implementation.
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
public final class Final<T> {
|
||||
private T mValue;
|
||||
|
||||
public Final() {
|
||||
}
|
||||
|
||||
public Final(T value) {
|
||||
this.mValue = value;
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void set(T value) {
|
||||
this.mValue = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
import com.microsoft.snippet.token.ILogToken;
|
||||
|
||||
/**
|
||||
* Log token Searcher. Not for external use
|
||||
*/
|
||||
interface ILogTokenSearcher {
|
||||
|
||||
/**
|
||||
* Return the token for the tag provided else return null
|
||||
*
|
||||
* @param tag tag for the log token
|
||||
* @return Log Token
|
||||
*/
|
||||
ILogToken search(String tag);
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.microsoft.snippet.token.ILogToken;
|
||||
import com.microsoft.snippet.token.LogTokenState;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* LogToken Pool is helps recycling the log token objects that are used by Snippet.
|
||||
*/
|
||||
public final class LogTokenPool {
|
||||
private static final String TAG = LogTokenPool.class.getSimpleName();
|
||||
|
||||
private final List<ILogToken> mPool = new LinkedList<>();
|
||||
|
||||
// A register to keep track of allocated tokens.
|
||||
private final HashSet<Integer> mRegister = new HashSet<>();
|
||||
|
||||
LogTokenPool() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
ILogToken obtain() {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "obtain() called");
|
||||
Log.d(TAG, "Number of objects in POOL while entering into obtain(): " + mPool.size());
|
||||
}
|
||||
ILogToken temp;
|
||||
synchronized (this) {
|
||||
if (mPool.size() > 0) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "Pool has reusable objects available. Will use one.");
|
||||
}
|
||||
temp = mPool.remove(0);
|
||||
boolean isAddingToRegisterSuccess = mRegister.add(temp.hashCode());
|
||||
if (isAddingToRegisterSuccess) {
|
||||
temp.setState(LogTokenState.ACTIVE);
|
||||
return temp;
|
||||
} else {
|
||||
Log.d(TAG, "Register already has the hashcode belonging to the current token, we need to recycle it and create a new one! ");
|
||||
return assureTokenWithUniqueHashCode(temp);
|
||||
}
|
||||
} else {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "Pool is empty, a new LogToken object will be created.");
|
||||
}
|
||||
temp = createTokenLocked();
|
||||
boolean isAddingToRegisterSuccess = mRegister.add(temp.hashCode());
|
||||
if (isAddingToRegisterSuccess) {
|
||||
temp.setState(LogTokenState.ACTIVE);
|
||||
return temp;
|
||||
} else {
|
||||
Log.d(TAG, "Register already has the hashcode belonging to the current token, we need to recycle it and create a new one! ");
|
||||
return assureTokenWithUniqueHashCode(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gives a one level safety against the situation where the hash codes of 2 log tokens
|
||||
* become same accidentally given the hash functions are not messed up. It recycles the old token
|
||||
* that has the same hash code that exists in the register and tries to generate a new token that
|
||||
* will be having different hash code.
|
||||
* If someone has messed the hash function badly. This will not help.
|
||||
*
|
||||
* @param oldToken old token that needs to get recycled.
|
||||
* @return new token
|
||||
*/
|
||||
@NonNull
|
||||
ILogToken assureTokenWithUniqueHashCode(ILogToken oldToken) {
|
||||
// Return old token to the pool
|
||||
oldToken.reset();
|
||||
mPool.add(oldToken);
|
||||
|
||||
// create a new token and return it.
|
||||
ILogToken newToken = createTokenLocked();
|
||||
newToken.setState(LogTokenState.ACTIVE);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
synchronized void recycle(@NonNull ILogToken token) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "recycle() called");
|
||||
Log.d(TAG, "Number of objects in POOL while entering into recycle(): " + mPool.size());
|
||||
}
|
||||
|
||||
// See if we are returning token which was not created using obtain()
|
||||
if (!mRegister.contains(token.hashCode())) {
|
||||
throw new IllegalStateException("Trying to return object which was not created using obtain() "
|
||||
+ "OR May be endCapture() was called multiple times on the same token object.");
|
||||
} else {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, " Recycling the LogToken object in the pool.");
|
||||
}
|
||||
token.reset();
|
||||
token.setState(LogTokenState.IN_POOL);
|
||||
mPool.add(token);
|
||||
mRegister.remove(token.hashCode());
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "Number of objects in POOL while after recycle(): " + mPool.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized ILogToken createTokenLocked() {
|
||||
return new Snippet.LogToken();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* A thread safe implementation which let's the caller thread to set the value of the wrapping
|
||||
* type once using {@link OneShot#set(Object)}, once the value is set, any other calls to set
|
||||
* is not honoured. The value remains the same for the lifetime of the instance.
|
||||
* Call {@link OneShot#get()} to access the wrapped instance.
|
||||
*
|
||||
* @param <T>
|
||||
* @author vishalratna
|
||||
*/
|
||||
public final class OneShot<T> {
|
||||
private static final String TAG = OneShot.class.getSimpleName();
|
||||
|
||||
private final AtomicReference<T> mData;
|
||||
private final AtomicInteger mCounter;
|
||||
|
||||
public OneShot(T data) {
|
||||
this.mData = new AtomicReference<>(data);
|
||||
this.mCounter = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
public void set(T data) {
|
||||
if (mCounter.compareAndSet(0, 1)) {
|
||||
T oldVal = mData.get();
|
||||
if (mData.compareAndSet(oldVal, data)) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "OneShot value set successfully.");
|
||||
}
|
||||
} else {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.e(TAG, "OneShot already set after the counter was set to 1. Cannot change the value again.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.e(TAG, "OneShot already set once. Cannot change the value again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return mData.get();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
/**
|
||||
* Represents a Pair of objects
|
||||
* @param <A> A
|
||||
* @param <B> B
|
||||
*/
|
||||
public final class Pair<A, B> {
|
||||
A a;
|
||||
B b;
|
||||
|
||||
public Pair(A first, B second) {
|
||||
this.a = first;
|
||||
this.b = second;
|
||||
}
|
||||
|
||||
public A getFirst() {
|
||||
return a;
|
||||
}
|
||||
|
||||
public B getSecond() {
|
||||
return b;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.microsoft.snippet.token.ILogToken;
|
||||
|
||||
/**
|
||||
* This is the NOOP execution path that is used by Release build types.
|
||||
*/
|
||||
public class ReleaseExecutionPath implements ExecutionPath {
|
||||
private static final ExecutionContext RESULT = new ExecutionContext();
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ExecutionContext capture(String message, Snippet.Closure closure) {
|
||||
closure.invoke();
|
||||
return RESULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ExecutionContext capture(Snippet.Closure closure) {
|
||||
closure.invoke();
|
||||
return RESULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken startCapture() {
|
||||
return Snippet.NO_OP_TOKEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken startCapture(String tag) {
|
||||
return Snippet.NO_OP_TOKEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken find(String tag) {
|
||||
return Snippet.NO_OP_TOKEN;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,791 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.microsoft.snippet.token.AttenuatedLogToken;
|
||||
import com.microsoft.snippet.token.ILogToken;
|
||||
import com.microsoft.snippet.token.LogTokenState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
* Snippet is a small library which can be used for measuring the time taken to execute a code
|
||||
* snippet. This library intends to reduce the boiler place code needed to add the start and end timestamps,
|
||||
* adding the execution context such as class,method and line number or any other relevant data and
|
||||
* printing the logs. The work is tedious and it becomes a nightmare when we have to do it across the
|
||||
* code base. The API provided by this library could be used to monitor PRs in the PR reviews where
|
||||
* the reviewer could ask for numbers for a specific snippet and compare the before and after numbers.
|
||||
* <h2> There are 2 ways to measure time in Snippet</h2>
|
||||
* <ol>
|
||||
* <li> Using <code>capture</code> APIs of Snippet</li>
|
||||
* <li> Using log tokens and <code>startCapture</code> and <code>endCapture</code> APIs </li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Example usage with <code>Snippet.capture</code>:
|
||||
* <pre>
|
||||
* {@code
|
||||
* Snippet.capture(() -> {
|
||||
* final ActionBar actionBar = getSupportActionBar();
|
||||
* if (actionBar != null && actionBar.isShowing()) {
|
||||
* if (mToolbar != null) {
|
||||
* ViewCompat.setElevation((View) mToolbar.getParent(), 0F);
|
||||
* mToolbar.animate()
|
||||
* .translationY(-mToolbar.getHeight())
|
||||
* .setInterpolator(new AccelerateDecelerateInterpolator())
|
||||
* .withEndAction(new Runnable() {
|
||||
* {@literal}Override
|
||||
* public void run() {
|
||||
* actionBar.hide();
|
||||
* if (mToolbarTranslateYListener != null) {
|
||||
* mToolbarTranslateYListener.onAnimationFinished();
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* .start();
|
||||
* } else {
|
||||
* actionBar.hide();
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* </pre>
|
||||
* Here library is going to measure time used to execute the code represented by the underlying lambda.
|
||||
* <p>
|
||||
* When you intent to measure the execution times for code which is not spread across multiple
|
||||
* methods and classes, capture APIs can be used. There is one caveat though, while capturing, we pass
|
||||
* a lambda for the closure representing the code snippet, this might be a problem if we want to capture
|
||||
* some non final variables outside the scope of lambda(Java does not support that). For that use, {@link Final} to create a
|
||||
* wrapper around your variable and use {@link Final#get()} and {@link Final#set(Object)}()} methods.
|
||||
* <p>
|
||||
* Another approach is through {@link Snippet#startCapture()} & {@link LogToken#endCapture()} APIs.
|
||||
* <code>startCapture</code> is going to return a token representing your execution it can be passed
|
||||
* from one component to another. The moment you call <code>endCapture()</code> it will measure the
|
||||
* time and print the logs.
|
||||
* <p>
|
||||
* Example usage of <code>Snippet.startCapture()</code> and <code>LogToken.endCapture()</code>:
|
||||
* <pre>
|
||||
* {@code
|
||||
* Snippet.LogToken token = Snippet.startCapture(); // start the measurement
|
||||
* getEditText().setOnTouchListener((v, event) -> {
|
||||
* mIsQueryTextFromVoiceInput = false;
|
||||
* if (!TextUtils.isEmpty(getEditText().getText())) {
|
||||
* updateClearIconAndVoiceEntryVisibility(VISIBLE, GONE);
|
||||
* }
|
||||
*
|
||||
* if (mOnEditTextTouchListener != null) {
|
||||
* return mOnEditTextTouchListener.onTouch(v, event);
|
||||
* }
|
||||
*
|
||||
* return false;
|
||||
* });
|
||||
*
|
||||
* initVoiceInputEntry(context);
|
||||
* token.endCapture(); // end measurement and print logs.
|
||||
* }
|
||||
* </pre>
|
||||
* We can use <code>startCapture()</code> method with a TAG also. This is particularly useful when
|
||||
* the execution is spread across multiple classes. We can use, {@link Snippet#find(String)} to find
|
||||
* the log token that was created with this tag. The logtoken received can be then used normally.
|
||||
* {@link LogToken} objects are internally obtained through a pool so, Snippet tries it best to
|
||||
* recycle the objects again and again.
|
||||
* <p>
|
||||
* There is concept of filter, which can be used to filter the logcat output. You can set the filter
|
||||
* using {@link Snippet#newFilter(String)}. Default filter is <b>Snippet</b>
|
||||
* While this is global filter for all the logs, you can still choose to have a different filter for a
|
||||
* particular LogToken using {@link LogToken#overrideFilter(String)} which will override the global filter.
|
||||
* <p>
|
||||
* Snippet can print Class, Method and Line number information in the logs as a part of execution
|
||||
* context. By default it prints class and method name. You can choose to have your combination of
|
||||
* details through {@link Snippet#addFlag(int)}
|
||||
* <b>Valid options are:</b>
|
||||
* <ol>
|
||||
*
|
||||
* <li>{@link Snippet#FLAG_METADATA_CLASS}</li>
|
||||
*
|
||||
* <li>{@link Snippet#FLAG_METADATA_METHOD}</li>
|
||||
*
|
||||
* <li>{@link Snippet#FLAG_METADATA_LINE}</li>
|
||||
*
|
||||
* <li>{@link Snippet#FLAG_NONE}</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* While these information should be enough but if you need to add some more information to the logs
|
||||
* like variable values, states etc then use {@link Snippet#capture(String, Closure)} and
|
||||
* {@link LogToken#endCapture(String)} overloads for adding custom messages to the logcat
|
||||
* output.
|
||||
* <p>
|
||||
* Snippet also supports the concept of <b>ThreadLocks</b>. ThreadLocks is a feature where a log token
|
||||
* can declare the thread creating the log token would only be able to call endCapture(). If other
|
||||
* thread tries to call endCapture(). It will log an error. To enable thread locks one should
|
||||
* call {@link LogToken#enableThreadLock()} and to check if the ThreadLock is enabled or not
|
||||
* {@link LogToken#isThreadLockEnabled()} can be used.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* There are times while you are measuring a sequence of code and you would like to measure how
|
||||
* much time some steps take within that sequence. As an example, while you are measuring some
|
||||
* method that takes 300ms to execute, it could have multiple areas inside it that could be adding up
|
||||
* to that number. So, to measure that {@link LogToken#addSplit()} and {@link LogToken#addSplit(String)}
|
||||
* could be used. Each call to add split print the time takes since the last addSplit() was called.
|
||||
* If addSplit() is called for the first time, then it would measure the time from startCapture().
|
||||
* </p>
|
||||
*
|
||||
* @author vishalratna
|
||||
*/
|
||||
public final class Snippet {
|
||||
private static final String TAG = LogToken.class.getSimpleName();
|
||||
static final AttenuatedLogToken NO_OP_TOKEN = new AttenuatedLogToken();
|
||||
private static final ExecutionContext EMPTY_CONTEXT = new ExecutionContext();
|
||||
public static final int FLAG_METADATA_CLASS = 1 << 31;
|
||||
public static final int FLAG_METADATA_METHOD = 1 << 30;
|
||||
public static final int FLAG_METADATA_LINE = 1 << 29;
|
||||
public static final int FLAG_METADATA_THREAD_INFO = 1 << 28;
|
||||
public static final int FLAG_NONE = 0;
|
||||
|
||||
private static String primaryFilter = Snippet.class.getSimpleName();
|
||||
private static final LogTokenPool OBJECT_POOL;
|
||||
private static String packageNameFilter = "com.microsoft";
|
||||
private static StackAnalyser stackAnalyser = new StackAnalyser(packageNameFilter);
|
||||
private static final TagHelper TAG_HELPER;
|
||||
private static int mFlags = FLAG_METADATA_CLASS | FLAG_METADATA_METHOD;
|
||||
private static final String SEPARATOR = "|::::|";
|
||||
private static final OneShot<Boolean> SHOULD_PRINT_DEBUG_LOGS = new OneShot<>(false);
|
||||
private static final OneShot<ExecutionPath> EXECUTION_PATH = new OneShot<ExecutionPath>(new ReleaseExecutionPath()); // Release is the default execution path
|
||||
static boolean mPrintDebugLogs = unBox(SHOULD_PRINT_DEBUG_LOGS.get()); // Do not set the value from anywhere other than turnOn/Off logs. Just meant for easy reference.
|
||||
|
||||
static {
|
||||
TAG_HELPER = new TagHelper();
|
||||
OBJECT_POOL = new LogTokenPool();
|
||||
}
|
||||
|
||||
private Snippet() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs custom execution path. It is best to call this as early as possible.
|
||||
* Prior to this call, core functionality will be routed to Release Execution path which is
|
||||
* default. Can be set once. Attempts to set it multiple times will not be honoured.
|
||||
*
|
||||
* @param path execution path.
|
||||
*/
|
||||
public static void install(ExecutionPath path) {
|
||||
EXECUTION_PATH.set(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a closure which needs to be measured.
|
||||
*
|
||||
* @param message Custom message if any.
|
||||
* @param closure Lambda or implementation representing the closure.
|
||||
*/
|
||||
public static void capture(String message, Closure closure) {
|
||||
EXECUTION_PATH.get().capture(message, closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a closure which needs to be measured when no custom messages are required.
|
||||
*
|
||||
* @param closure Lambda or implementation representing the closure.
|
||||
*/
|
||||
public static void capture(Closure closure) {
|
||||
EXECUTION_PATH.get().capture(null, closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snippet identifies the execution context by analysing the stack frames and examining private
|
||||
* members of {@link StackTraceElement} class. Out of dozens of stack frames containing JDK
|
||||
* and android SDK frames, it has to identify the last frame which was related to the user application.
|
||||
* For that uses the regex matching this filter to identify the user's application frame.
|
||||
* After receiving the new regex it replaces the old stack analyser component with the new one.
|
||||
* This impacts stack analyser of the log token also. Any log token created after this call will
|
||||
* use the new regex, but token created before this call will have this one.
|
||||
*
|
||||
* @param regex REGEX identifying the application.
|
||||
*/
|
||||
public static void setPackageRegex(String regex) {
|
||||
packageNameFilter = regex;
|
||||
stackAnalyser = new StackAnalyser(packageNameFilter);
|
||||
}
|
||||
|
||||
private static ExecutionContext invokeMeasureAndAttachExecutionContext(String message, Closure closure) {
|
||||
long delta = ToolBox.invokeAndMeasure(closure);
|
||||
ExecutionContext executionContext = getExecutionContext();
|
||||
executionContext.setExecutionDuration(delta);
|
||||
|
||||
// Build the log string using the snippet info we got.
|
||||
StringBuilder logMessageBuilder = new StringBuilder();
|
||||
if (message != null && !message.isEmpty()) {
|
||||
logMessageBuilder.append(message).append("::");
|
||||
}
|
||||
|
||||
appendExecutionContextToLog(logMessageBuilder, executionContext);
|
||||
|
||||
logMessageBuilder.append(SEPARATOR).append('(').append(delta).append(" ms)");
|
||||
Log.d(primaryFilter, logMessageBuilder.toString());
|
||||
return executionContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the execution context in te form of SnippetInfo class. That can be returned and
|
||||
* used by external clients too.
|
||||
*
|
||||
* @return snippet info.
|
||||
*/
|
||||
private static ExecutionContext getExecutionContext() {
|
||||
|
||||
Thread thread = Thread.currentThread();
|
||||
|
||||
ExecutionContext info = new ExecutionContext();
|
||||
info.setClassName(stackAnalyser.callingClass(thread, StackAnalyser.API_CAPTURE));
|
||||
info.setMethod(stackAnalyser.callingMethod(thread, StackAnalyser.API_CAPTURE));
|
||||
info.setLineNo(stackAnalyser.callingLine(thread, StackAnalyser.API_CAPTURE));
|
||||
info.setThreadName(thread.getName());
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private static void appendExecutionContextToLog(StringBuilder logMessageBuilder, ExecutionContext context) {
|
||||
if (testFlag(FLAG_METADATA_CLASS)) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "FLAG_METADATA_CLASS set");
|
||||
}
|
||||
String trimmedClass = trimPackageFromClass(context.getClassName());
|
||||
logMessageBuilder.append("[Class = ").append(trimmedClass).append(']').append(SEPARATOR);
|
||||
}
|
||||
if (testFlag(FLAG_METADATA_METHOD)) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "FLAG_METADATA_METHOD set");
|
||||
}
|
||||
logMessageBuilder.append("[Method = ").append(context.getMethodName()).append(']').append(SEPARATOR);
|
||||
}
|
||||
if (testFlag(FLAG_METADATA_LINE)) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "FLAG_METADATA_LINE set");
|
||||
}
|
||||
logMessageBuilder.append("<Line no. ").append(context.getLineNo()).append('>').append(SEPARATOR);
|
||||
}
|
||||
if (testFlag(FLAG_METADATA_THREAD_INFO)) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "FLAG_METADATA_THREAD_INFO set");
|
||||
}
|
||||
logMessageBuilder.append("[Thread name = ").append(Thread.currentThread().getName()).append(']').append(SEPARATOR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static String trimPackageFromClass(String qualifiedName) {
|
||||
String[] tokens = qualifiedName.split("\\.");
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
StringBuilder temp = new StringBuilder();
|
||||
for (String a : tokens) {
|
||||
temp.append('[').append(a).append("] ");
|
||||
}
|
||||
Log.d(TAG, "trimPackageFromClass() tokens: " + temp.toString());
|
||||
}
|
||||
return tokens[tokens.length - 1];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a new global filter and returns the old filter, just in case if you need to restore it.
|
||||
*
|
||||
* @param newFilterKey new filter for logcat
|
||||
* @return old filter value.
|
||||
*/
|
||||
public static String newFilter(String newFilterKey) {
|
||||
String oldTag = primaryFilter;
|
||||
primaryFilter = newFilterKey;
|
||||
return oldTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a flag which determines the execution context shown on the logs such as class name, method
|
||||
* and line no getting called on the logcat output. Supports
|
||||
* <ol>
|
||||
* * <li>{@link Snippet#FLAG_METADATA_CLASS}</li>
|
||||
* * <li>{@link Snippet#FLAG_METADATA_METHOD}</li>
|
||||
* * <li>{@link Snippet#FLAG_METADATA_LINE}</li>
|
||||
* * <li>{@link Snippet#FLAG_NONE}</li>
|
||||
* * </ol>
|
||||
*
|
||||
* @param flag Integer representing the flags
|
||||
* @return new flag value.
|
||||
*/
|
||||
public static int addFlag(int flag) {
|
||||
//assureCorrectFlag(flag); // This will prevent us from supplying compound flags.
|
||||
mFlags |= flag;
|
||||
return mFlags;
|
||||
}
|
||||
|
||||
private static void assureCorrectFlag(int flag) {
|
||||
if (flag != 1 << 31 && flag != 1 << 30 && flag != 1 << 29 && flag != 1 << 28) {
|
||||
throw new IllegalArgumentException("Please set a valid flag");
|
||||
}
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(TAG, "Flag validation completed.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a particular flag is set in the Snippet or not.
|
||||
*
|
||||
* @param flag Flag to test
|
||||
* @return true if set, false otherwise.
|
||||
*/
|
||||
public static boolean testFlag(int flag) {
|
||||
assureCorrectFlag(flag);
|
||||
return (mFlags & flag) == flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the flag which shows execution context.
|
||||
* Calling this will not show any execution context in the logs.
|
||||
*/
|
||||
public static void clearFlags() {
|
||||
mFlags = FLAG_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the measurement of code spread across multiple classes and methods. It returns a
|
||||
* LogToken which can be passed across class and methods. To end the measurement call {@link LogToken#endCapture()}
|
||||
*
|
||||
* @return LogToken
|
||||
*/
|
||||
@NonNull
|
||||
public static ILogToken startCapture() {
|
||||
return EXECUTION_PATH.get().startCapture();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the measurement of code spread across multiple classes and methods. It returns a
|
||||
* LogToken which can be passed as well as retrieved using a tag which is provided at the
|
||||
* time of token creation. It enables a direct access for the log token anywhere in the code.
|
||||
* If a token with the provided tag already exists, this call with return null which will make
|
||||
* further calls to {@link LogToken#endCapture()} crash.
|
||||
* If multiple threads simultaneously try to call this method with same tag, the first one to acquire the tag
|
||||
* wins and the other gets Pair object with supplied log token and a successFlag as false .
|
||||
* The token acquired in the initial step of this method is returned to the pool.
|
||||
*
|
||||
* @param tag tag
|
||||
* @return LogToken which can we retrieved using the tag if request successful, null otherwise
|
||||
*/
|
||||
@Nullable
|
||||
public static ILogToken startCapture(String tag) {
|
||||
return EXECUTION_PATH.get().startCapture(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a token with provided tag. The tag should match the tag provided with
|
||||
* {@link Snippet#startCapture(String)} or else Attenuated token is returned.
|
||||
*
|
||||
* @param tag Tag
|
||||
* @return LogToken if existing, attenuated token otherwise.
|
||||
*/
|
||||
public static ILogToken find(String tag) {
|
||||
return EXECUTION_PATH.get().find(tag);
|
||||
}
|
||||
|
||||
public static void turnOnLogging() {
|
||||
SHOULD_PRINT_DEBUG_LOGS.set(true);
|
||||
mPrintDebugLogs = unBox(SHOULD_PRINT_DEBUG_LOGS.get());
|
||||
}
|
||||
|
||||
public static void turnOffLogging() {
|
||||
SHOULD_PRINT_DEBUG_LOGS.set(false);
|
||||
mPrintDebugLogs = unBox(SHOULD_PRINT_DEBUG_LOGS.get());
|
||||
}
|
||||
|
||||
private static boolean unBox(Boolean aBoolean) {
|
||||
if (aBoolean == null) {
|
||||
return false;
|
||||
}
|
||||
return aBoolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Piece of code whose execution times need to be measured.
|
||||
*/
|
||||
public interface Closure {
|
||||
/**
|
||||
* Invokes the code which is passed inside the closure.
|
||||
* Users are not supposed to call this method directly.
|
||||
*/
|
||||
void invoke();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the execution spreads across multiple classes and methods, and lambda cannot be used
|
||||
* to capture the code, then LogToken is created by calling {@link Snippet#startCapture()}, the
|
||||
* measurement start from this point, now we can pass the token anywhere
|
||||
* and, will end the measurement when {@link LogToken#endCapture()} is called.
|
||||
* Default filter for a log token is the global filter of Snippet.
|
||||
*/
|
||||
public static class LogToken implements ILogToken {
|
||||
private static final String TAG = LogToken.class.getSimpleName();
|
||||
private static final String SPLIT_MESSAGE = "********SPLIT[" + "%1s" + "]"
|
||||
+ SEPARATOR + "(" + "%2s" + " " + "ms"
|
||||
+ ")" + "********";
|
||||
|
||||
private final StackAnalyser mLocalAnalyser = new StackAnalyser(packageNameFilter);
|
||||
private long mStartTime;
|
||||
private long mEndTime;
|
||||
private String mFilter;
|
||||
private long mThreadId = -1L;
|
||||
private boolean mThreadLockEnabled = false;
|
||||
private volatile LogTokenState mState;
|
||||
private long mLastSplitTimeCaptured = 0L;
|
||||
private List<Split> mSplitRecord;
|
||||
|
||||
// To be called only through LogTokenPool. Should not be created through any other ways.
|
||||
protected LogToken() {
|
||||
if (mPrintDebugLogs) {
|
||||
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
|
||||
Log.d(TAG, "Inside LogToken's <init>. startCapture() called on the main thread. LogToken[" + this.toString() + "]");
|
||||
} else {
|
||||
Log.d(TAG, "Inside LogToken's <init>. startCapture() called off the main thread. LogToken[" + this.toString() + "]");
|
||||
}
|
||||
Log.d(TAG, "<init> will be called once per LogToken object, after the usage is over, it will be returned to the pool and will get recycled using obtain()");
|
||||
}
|
||||
this.mStartTime = ToolBox.currentTime();
|
||||
this.mFilter = Snippet.primaryFilter; // Uses the primary filter by default.
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to override the Global filter provided by Snippet.
|
||||
* Once this LogToken's endCapture() is called, the filter is reset to
|
||||
* the global filter again.
|
||||
*
|
||||
* @param newFilter new filter
|
||||
* @return LogToken instance on which start()/end() can be called.
|
||||
*/
|
||||
@Override
|
||||
public LogToken overrideFilter(String newFilter) {
|
||||
this.mFilter = newFilter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current filter
|
||||
*
|
||||
* @return existing filter
|
||||
*/
|
||||
@Override
|
||||
public String filter() {
|
||||
return this.mFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long creatorThreadId() {
|
||||
return this.mThreadId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isThreadLockEnabled() {
|
||||
return mThreadLockEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken enableThreadLock() {
|
||||
mThreadLockEnabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setCreatorThreadId(long id) {
|
||||
this.mThreadId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.mStartTime = 0;
|
||||
this.mEndTime = 0;
|
||||
this.mFilter = Snippet.primaryFilter;
|
||||
this.mThreadId = -1L;
|
||||
this.mThreadLockEnabled = false;
|
||||
if(this.mSplitRecord != null) {
|
||||
this.mSplitRecord.clear();
|
||||
}
|
||||
this.mSplitRecord = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a split within the span of LogToken's startCapture() and endCapture() methods.
|
||||
* Calling addSplit() will measure the time taken since the last time addSplit() was called.
|
||||
* If addSplit() is called for the first time then it would measure the time between
|
||||
* {@link Snippet#startCapture()} and {@link LogToken#endCapture()}. Calling addSplit() after
|
||||
* endCapture() is called would lead to IllegalStateException.
|
||||
*/
|
||||
@Override
|
||||
public void addSplit() {
|
||||
Split newSplit = addSplitInternal();
|
||||
Log.d(mFilter, String.format(SPLIT_MESSAGE, newSplit.sequence(), newSplit.delta()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the same thing as {@link LogToken#addSplit()}. It just additionally adds a message string
|
||||
* with the Split.
|
||||
*
|
||||
* @param message Custom message to print on the log cat.
|
||||
*/
|
||||
@Override
|
||||
public void addSplit(String message) {
|
||||
Split newSplit = addSplitInternal();
|
||||
newSplit.setName(message);
|
||||
Log.d(mFilter, String.format(SPLIT_MESSAGE, "[" + newSplit.sequence() + "]" + message, newSplit.delta()));
|
||||
}
|
||||
|
||||
private Split addSplitInternal() {
|
||||
// This is not fully true but a quick hack, will have to think about multiple states possible.
|
||||
if (mState != LogTokenState.ACTIVE) {
|
||||
throw new IllegalStateException("addSplit() called after endCapture() is executed! Development error!!!!!!");
|
||||
}
|
||||
Split newSplit = createSplit();
|
||||
synchronized (this) {
|
||||
if (mSplitRecord == null) {
|
||||
mSplitRecord = new ArrayList<>();
|
||||
}
|
||||
mSplitRecord.add(newSplit);
|
||||
}
|
||||
return newSplit;
|
||||
}
|
||||
|
||||
private Split createSplit() {
|
||||
Split newSplit;
|
||||
synchronized (this) {
|
||||
if (mState != LogTokenState.ACTIVE) {
|
||||
throw new IllegalStateException("addSplit() called after endCapture() is executed! Development error!!!!!!");
|
||||
}
|
||||
if (mLastSplitTimeCaptured == 0L) { // Split called for the first time
|
||||
// We use the token start time as the reference.
|
||||
mLastSplitTimeCaptured = ToolBox.currentTime();
|
||||
newSplit = new Split(getStart(), mLastSplitTimeCaptured);
|
||||
} else {
|
||||
// Here we use the last split time captured.
|
||||
long currentTime = ToolBox.currentTime();
|
||||
newSplit = new Split(mLastSplitTimeCaptured, currentTime);
|
||||
mLastSplitTimeCaptured = currentTime;
|
||||
}
|
||||
}
|
||||
return newSplit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setState(LogTokenState state) {
|
||||
mState = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogTokenState getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the capture which was started through {@link Snippet#startCapture()}.
|
||||
*/
|
||||
@Override
|
||||
public ExecutionContext endCapture() {
|
||||
synchronized (this) {
|
||||
if (mState == LogTokenState.END_CAPTURE_EXECUTED) {
|
||||
return Snippet.EMPTY_CONTEXT;
|
||||
}
|
||||
ExecutionContext executionContext;
|
||||
if (ToolBox.willThreadLockGuardThisCapture(Thread.currentThread(), this)) {
|
||||
Log.e(TAG, "ThreadLocks enabled! Not able to end the capture as the token "
|
||||
+ "creating thread is not same as the thread calling endCapture().");
|
||||
|
||||
return Snippet.EMPTY_CONTEXT;
|
||||
} else {
|
||||
mState = LogTokenState.END_CAPTURE_EXECUTED;
|
||||
executionContext = doEndSlice(null);
|
||||
if (mSplitRecord != null && mSplitRecord.size() > 0) {
|
||||
dumpSplitData(mSplitRecord, executionContext);
|
||||
}
|
||||
ILogToken token = TAG_HELPER.unTag(this);
|
||||
if (token == null) {
|
||||
Log.e(TAG, "Not able to unTag as the tag for the request was not available.");
|
||||
}
|
||||
OBJECT_POOL.recycle(this);
|
||||
return executionContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dumpSplitData(List<Split> splits, ExecutionContext context) {
|
||||
StringBuilder recordSummaryBuilder = new StringBuilder('\n');
|
||||
recordSummaryBuilder.append(" Split Summary").append('\n');
|
||||
recordSummaryBuilder.append(" *********************************************************************************************"
|
||||
+ "*************************************");
|
||||
for (Split split : splits) {
|
||||
recordSummaryBuilder.append('\n').append('|').append("___").append("Split[").append(split.sequence()).append(']');
|
||||
if (split.getName() != null && !TextUtils.isEmpty(split.getName())) {
|
||||
recordSummaryBuilder.append('[').append(split.getName()).append(']').append(' ');
|
||||
}
|
||||
recordSummaryBuilder.append(split.delta()).append('/').append(context.getExecutionDuration()).append(" ( ms ) ");
|
||||
recordSummaryBuilder.append(" ").append('(').append(String.format(Locale.US, "%.3f", split.percentage(context.getExecutionDuration()))).append(" %").append(')');
|
||||
recordSummaryBuilder.append(" of total capture.");
|
||||
}
|
||||
recordSummaryBuilder.append("\n *************************************************************************************************************************************");
|
||||
Log.d(mFilter, recordSummaryBuilder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the capture which was started through {@link Snippet#startCapture()}.
|
||||
*
|
||||
* @param message Custom message if required
|
||||
*/
|
||||
@Override
|
||||
public ExecutionContext endCapture(String message) {
|
||||
synchronized (this) {
|
||||
if (mState == LogTokenState.END_CAPTURE_EXECUTED) {
|
||||
return Snippet.EMPTY_CONTEXT;
|
||||
}
|
||||
ExecutionContext executionContext;
|
||||
if (ToolBox.willThreadLockGuardThisCapture(Thread.currentThread(), this)) {
|
||||
Log.e(TAG, mFilter + " ThreadLocks enabled! Not able to end the capture as the"
|
||||
+ " token creating thread is not same as the thread calling endCapture(message).");
|
||||
|
||||
return Snippet.EMPTY_CONTEXT;
|
||||
} else {
|
||||
mState = LogTokenState.END_CAPTURE_EXECUTED;
|
||||
executionContext = doEndSlice(message);
|
||||
if (mSplitRecord != null && mSplitRecord.size() > 0) {
|
||||
dumpSplitData(mSplitRecord, executionContext);
|
||||
}
|
||||
ILogToken token = TAG_HELPER.unTag(this);
|
||||
if (token == null) {
|
||||
Log.d(TAG, "Not able to unTag as the tag for the request was not available.");
|
||||
}
|
||||
OBJECT_POOL.recycle(this);
|
||||
return executionContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ExecutionContext doEndSlice(String message) {
|
||||
synchronized (this) {
|
||||
mEndTime = ToolBox.currentTime();
|
||||
long delta = mEndTime - mStartTime;
|
||||
|
||||
StringBuilder logMessageBuilder = new StringBuilder();
|
||||
if (message != null && !message.isEmpty()) {
|
||||
logMessageBuilder.append(message).append(SEPARATOR);
|
||||
}
|
||||
Thread thread = Thread.currentThread();
|
||||
if (mPrintDebugLogs) {
|
||||
if (thread == Looper.getMainLooper().getThread()) {
|
||||
Log.d(TAG, "endCapture() called on the main thread. LogToken[" + this.toString() + "]");
|
||||
} else {
|
||||
Log.d(TAG, "endCapture() called off the main thread");
|
||||
}
|
||||
}
|
||||
ExecutionContext executionContext = new ExecutionContext();
|
||||
executionContext.setClassName(mLocalAnalyser.callingClass(thread, StackAnalyser.API_LOG_TOKEN));
|
||||
executionContext.setMethod(mLocalAnalyser.callingMethod(thread, StackAnalyser.API_LOG_TOKEN));
|
||||
executionContext.setLineNo(mLocalAnalyser.callingLine(thread, StackAnalyser.API_LOG_TOKEN));
|
||||
executionContext.setThreadName(thread.getName());
|
||||
executionContext.setExecutionDuration(delta);
|
||||
|
||||
appendExecutionContextToLog(logMessageBuilder, executionContext);
|
||||
logMessageBuilder.append(SEPARATOR).append('(').append(delta).append(" ms)");
|
||||
Log.d(mFilter, logMessageBuilder.toString());
|
||||
return executionContext;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getStart() {
|
||||
return mStartTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getEnd() {
|
||||
return mEndTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setStart(long start) {
|
||||
this.mStartTime = start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setEnd(long start) {
|
||||
this.mStartTime = start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execution path that is used by Snippet to route the code to the core library functionality.
|
||||
*/
|
||||
public static class MeasuredExecutionPath implements ExecutionPath {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ExecutionContext capture(String message, Closure closure) {
|
||||
return invokeMeasureAndAttachExecutionContext(message, closure);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ExecutionContext capture(Closure closure) {
|
||||
return invokeMeasureAndAttachExecutionContext(null, closure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken startCapture() {
|
||||
long startTime = ToolBox.currentTime();
|
||||
ILogToken token = OBJECT_POOL.obtain();
|
||||
token.setStart(startTime);
|
||||
token.setCreatorThreadId(Thread.currentThread().getId());
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken startCapture(String tag) {
|
||||
long startTime = ToolBox.currentTime();
|
||||
ILogToken token = OBJECT_POOL.obtain();
|
||||
token.setStart(startTime);
|
||||
token.setCreatorThreadId(Thread.currentThread().getId());
|
||||
Pair<ILogToken, Boolean> tagResult = TAG_HELPER.tag(tag, token);
|
||||
if (!tagResult.getSecond()) {
|
||||
Log.e(TAG, "Tag: [" + tag + "] already exists in the record, cannot assign log token, so we are providing a NO_OP_TOKEN.");
|
||||
OBJECT_POOL.recycle(token);
|
||||
return NO_OP_TOKEN;
|
||||
}
|
||||
if (mPrintDebugLogs) {
|
||||
Log.e(TAG, "Tag: [" + tag + "] created for the LogToken.");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to find Log Token that was created using {@link Snippet#startCapture(String)}
|
||||
* If we try to find a tag that does not exist it would return {@link Snippet#NO_OP_TOKEN}
|
||||
* that is a NO-OP implementation of {@link ILogToken} interface.
|
||||
*
|
||||
* @param tag Custom tag. This tag will be used to search the log token across the app.
|
||||
* @return LogToken if existing with the tag, No op token otherwise.
|
||||
*/
|
||||
@Override
|
||||
public ILogToken find(String tag) {
|
||||
ILogToken token = TAG_HELPER.search(tag);
|
||||
if (token == null) {
|
||||
return NO_OP_TOKEN;
|
||||
} else {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
/**
|
||||
* Contains the list of areas that are tracked by Snippet. These will help getting the before/after
|
||||
* numbers while doing the PR review.
|
||||
*/
|
||||
public final class SnippetConstants {
|
||||
|
||||
private SnippetConstants() {
|
||||
// private
|
||||
}
|
||||
|
||||
// TTE stands for Time to Execute
|
||||
public static final String SUPER_ON_MAM_CREATE = "TTE super.onMamCreate()";
|
||||
public static final String INIT_SKYPE_DB_HELPER = "TTE SkypeTeamsDBHelper";
|
||||
public static final String INIT_BACKGROUND_OBJ = "TTE Initialize background objects";
|
||||
public static final String INIT_NOTIFICATION_CHANNEL_HELPER = "TTE Initialize notificationChannelHelper";
|
||||
public static final String INIT_NOTIFICATION_MGR = "TTE Initialize notification manager";
|
||||
public static final String SUPER_MA_ON_CREATE = "TTE super.onCreate() MA";
|
||||
public static final String INIT_SEARCH_BAR_VIEW = "TTE SearchBarView MA";
|
||||
public static final String SETUP_CONTENT_VIEW = "TTE SetupContentView";
|
||||
public static final String INJECT_DEPENDENCIES = "TTE injectIfNecessary";
|
||||
public static final String SETUP_TOOLBAR = "TTE setupToolBar";
|
||||
public static final String MA_LOAD_INACTIVE_TABS_UI_THREAD_WORK = "TTE loadInactiveTabs() UI thread Work";
|
||||
public static final String MA_LOAD_SELECTED_FRAGMENT = "TTE load selected fragment";
|
||||
public static final String DEPENDENCY_INJECTION = "TTE Dependency injection";
|
||||
public static final String BOTTOM_BAR_APP_CLICK_TELEMETRY = "Bottom Bar App Click Telemetry";
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.microsoft.snippet;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class Split {
|
||||
|
||||
private static final AtomicInteger SEQUENCE = new AtomicInteger(1);
|
||||
private final long mStarted;
|
||||
private final long mEnded;
|
||||
private final int mSequence;
|
||||
private String mName;
|
||||
|
||||
private String mInfo;
|
||||
|
||||
public Split(long start, long end) {
|
||||
this.mStarted = start;
|
||||
this.mEnded = end;
|
||||
this.mSequence = SEQUENCE.getAndIncrement();
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.mName = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.mName;
|
||||
}
|
||||
|
||||
public long getStarted() {
|
||||
return mStarted;
|
||||
}
|
||||
|
||||
public long getEnded() {
|
||||
return mEnded;
|
||||
}
|
||||
|
||||
public long delta() {
|
||||
return mEnded - mStarted;
|
||||
}
|
||||
|
||||
public String getInfo() {
|
||||
return mInfo;
|
||||
}
|
||||
|
||||
public void setInfo(String mInfo) {
|
||||
this.mInfo = mInfo;
|
||||
}
|
||||
|
||||
public int sequence() {
|
||||
return this.mSequence;
|
||||
}
|
||||
|
||||
public double percentage(long total) {
|
||||
return ((double) delta() / total) * 100;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
/**
|
||||
* Internal helper class used to extract the execution context of the code which was guarded
|
||||
* by Snippet APIs.
|
||||
* NOT FOR EXTERNAL USE
|
||||
*/
|
||||
final class StackAnalyser {
|
||||
static final int API_CAPTURE = 0;
|
||||
static final int API_LOG_TOKEN = 1;
|
||||
|
||||
private final String mPackage;
|
||||
|
||||
StackAnalyser(String packageName) {
|
||||
this.mPackage = packageName;
|
||||
}
|
||||
|
||||
StackAnalyser() {
|
||||
this("com.microsoft");
|
||||
}
|
||||
|
||||
String callingMethod(Thread mCallingThread, int apiType) {
|
||||
if (apiType == API_CAPTURE) {
|
||||
return getCallingFrameForCapture(mCallingThread, apiType).getMethodName();
|
||||
} else {
|
||||
return getDoEndSliceCallerFrame(mCallingThread, apiType).getMethodName();
|
||||
}
|
||||
}
|
||||
|
||||
String callingClass(Thread mCallingThread, int apiType) {
|
||||
if (apiType == API_CAPTURE) {
|
||||
return getCallingFrameForCapture(mCallingThread, apiType).getClassName();
|
||||
} else {
|
||||
return getDoEndSliceCallerFrame(mCallingThread, apiType).getClassName();
|
||||
}
|
||||
}
|
||||
|
||||
int callingLine(Thread mCallingThread, int apiType) {
|
||||
if (apiType == API_CAPTURE) {
|
||||
return getCallingFrameForCapture(mCallingThread, apiType).getLineNumber();
|
||||
} else {
|
||||
return getDoEndSliceCallerFrame(mCallingThread, apiType).getLineNumber();
|
||||
}
|
||||
}
|
||||
|
||||
private StackTraceElement searchForCallingFrameOfMethodCaller(Thread callingThread, String methodName, int apiType) {
|
||||
StackTraceElement[] frames = callingThread.getStackTrace();
|
||||
int index = -1;
|
||||
for (int i = 0; i < frames.length; i++) {
|
||||
if (frames[i].getMethodName().equals(methodName)
|
||||
&& frames[i].getClassName().startsWith(mPackage) && apiType == API_CAPTURE
|
||||
&& frames[i].getClassName().equals("com.microsoft.snippet.Snippet")
|
||||
||
|
||||
frames[i].getMethodName().equals(methodName)
|
||||
&& frames[i].getClassName().startsWith(mPackage) && apiType == API_LOG_TOKEN
|
||||
&& frames[i].getClassName().equals("com.microsoft.snippet.Snippet$LogToken")) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Now we know the index at which we found the method in the stackframe, the next stack frame belongs to the code that called that method.
|
||||
return callingThread.getStackTrace()[index + 1];
|
||||
}
|
||||
|
||||
private StackTraceElement getDoEndSliceCallerFrame(Thread thread, int apiType) {
|
||||
return searchForCallingFrameOfMethodCaller(thread, "endCapture", apiType);
|
||||
}
|
||||
|
||||
private StackTraceElement getCallingFrameForCapture(Thread thread, int apiType) {
|
||||
return searchForCallingFrameOfMethodCaller(thread, "capture", apiType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.microsoft.snippet.token.ILogToken;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Internal helper class that takes care of tagging the LogTokens.
|
||||
* Not intended for external use.
|
||||
*
|
||||
* @author vishalratna
|
||||
*/
|
||||
class TagHelper implements ILogTokenSearcher {
|
||||
private static final String LOG_TAG = TagHelper.class.getSimpleName();
|
||||
|
||||
private final Map<String, ILogToken> mRegistry;
|
||||
|
||||
TagHelper() {
|
||||
this.mRegistry = new HashMap<>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Pair<ILogToken, Boolean> tag(String tag, ILogToken token) {
|
||||
synchronized (mRegistry) {
|
||||
// Synchronizing to make sure, while we are checking the existence, a new thread does
|
||||
// not add a key which returns true for contains() call.
|
||||
if (mRegistry.containsKey(tag)) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.e(LOG_TAG, "Tag already existing, we will not provide tagged token. Returning the token without tagging.");
|
||||
}
|
||||
return new Pair<>(token, false);
|
||||
}
|
||||
|
||||
// Now we know that entry is not existing, make an entry
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(LOG_TAG, "Tagging the LogToken and returning it back");
|
||||
}
|
||||
mRegistry.put(tag, token);
|
||||
}
|
||||
|
||||
return new Pair<>(token, true);
|
||||
}
|
||||
|
||||
ILogToken unTag(ILogToken token) {
|
||||
synchronized (mRegistry) {
|
||||
String requestedTag = null;
|
||||
Set<Map.Entry<String, ILogToken>> entrySet = mRegistry.entrySet();
|
||||
for (Map.Entry<String, ILogToken> eachEntry : entrySet) {
|
||||
if (eachEntry.getValue() == token) {
|
||||
requestedTag = eachEntry.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we did not get anything after searching the registry.
|
||||
if (requestedTag == null) {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.e(LOG_TAG, "There is no such tagged LogToken existing. Bad request! Returning");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.e(LOG_TAG, "We found a existing tag for the LogToken " + token.toString() + ", removing the tag");
|
||||
}
|
||||
return mRegistry.remove(requestedTag);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken search(String tag) {
|
||||
synchronized (mRegistry) {
|
||||
if (mRegistry.containsKey(tag)) {
|
||||
return mRegistry.get(tag);
|
||||
} else {
|
||||
if (Snippet.mPrintDebugLogs) {
|
||||
Log.d(LOG_TAG, "There is no log token with tag: " + tag);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.microsoft.snippet.token.ILogToken;
|
||||
|
||||
/**
|
||||
* Utility class the houses some helper functions used across the library.
|
||||
*/
|
||||
final class ToolBox {
|
||||
|
||||
private ToolBox() {
|
||||
|
||||
}
|
||||
|
||||
static long invokeAndMeasure(Snippet.Closure closure) {
|
||||
long start = SystemClock.uptimeMillis();
|
||||
closure.invoke();
|
||||
long end = SystemClock.uptimeMillis();
|
||||
|
||||
return end - start;
|
||||
}
|
||||
|
||||
static boolean willThreadLockGuardThisCapture(Thread currentThread, ILogToken token) {
|
||||
if (token.isThreadLockEnabled()) {
|
||||
return currentThread.getId() != token.creatorThreadId();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static String combineThreadIdWithUserTag(String tag) {
|
||||
return tag + ":" + Thread.currentThread().getId();
|
||||
}
|
||||
|
||||
static long getThreadIdFromSnippetTag(String snippetTag) {
|
||||
String[] items = snippetTag.split(":");
|
||||
if (items.length != 2) {
|
||||
throw new IllegalStateException("Snippet tag should have 2 items, user tag and thread ID");
|
||||
}
|
||||
return Long.parseLong(items[1]);
|
||||
}
|
||||
|
||||
static String getUserTagFromSnippetTag(String snippetTag) {
|
||||
String[] items = snippetTag.split(":");
|
||||
if (items.length != 2) {
|
||||
throw new IllegalStateException("Snippet tag should have 2 items, user tag and thread ID");
|
||||
}
|
||||
return items[0];
|
||||
}
|
||||
|
||||
static long currentTime() {
|
||||
return SystemClock.uptimeMillis();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet;
|
||||
|
||||
/**
|
||||
* Represents a Triple of objects
|
||||
*
|
||||
* @param <A> A
|
||||
* @param <B> B
|
||||
* @param <C> C
|
||||
*/
|
||||
public final class Triple<A, B, C> {
|
||||
A a;
|
||||
B b;
|
||||
C c;
|
||||
|
||||
public Triple(A first, B second, C third) {
|
||||
this.a = first;
|
||||
this.b = second;
|
||||
this.c = third;
|
||||
}
|
||||
|
||||
public A getFirst() {
|
||||
return a;
|
||||
}
|
||||
|
||||
public B getSecond() {
|
||||
return b;
|
||||
}
|
||||
|
||||
public C getThird() {
|
||||
return c;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet.token;
|
||||
|
||||
import com.microsoft.snippet.ExecutionContext;
|
||||
|
||||
/**
|
||||
* Attenuated token represents a LogToken with suppressed functionality.
|
||||
* This is helpful in
|
||||
* 1. Creating NO-OP implementations for our public facing APIs.
|
||||
* 2. While using {@link com.microsoft.snippet.Snippet#find(String)} it might be possible that we
|
||||
* ask for a tag that does not exist it would lead to a null result and would require the caller
|
||||
* to use a null check. Using this there helps in returning a no op token and user can avoid null check
|
||||
* 3. In some flows you won't always find logtokens for example,
|
||||
* while creating a token with tag in onCreate() of Application class and trying to fetch it in
|
||||
* some activity. As application onCreate() will not be called always, activity might need a
|
||||
* null check after find call, that is avoided by the use of this class.
|
||||
*/
|
||||
public final class AttenuatedLogToken implements ILogToken {
|
||||
private static final ExecutionContext NONE_INFO = new ExecutionContext();
|
||||
|
||||
public AttenuatedLogToken() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionContext endCapture() {
|
||||
return NONE_INFO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStart() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEnd() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStart(long start) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnd(long start) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken overrideFilter(String newFilter) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionContext endCapture(String message) {
|
||||
return NONE_INFO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long creatorThreadId() {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isThreadLockEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILogToken enableThreadLock() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreatorThreadId(long threadId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSplit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSplit(String message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setState(LogTokenState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogTokenState getState() {
|
||||
return LogTokenState.ATTENUATED;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet.token;
|
||||
|
||||
import com.microsoft.snippet.Snippet;
|
||||
import com.microsoft.snippet.ExecutionContext;
|
||||
|
||||
/**
|
||||
* Representation of log token. All types of log token should implement this interface
|
||||
*/
|
||||
public interface ILogToken {
|
||||
/**
|
||||
* Set the filter for this log token, this is override the global filter.
|
||||
*
|
||||
* @param newFilter new filter
|
||||
* @return IToken instance on which start()/end() can be called.
|
||||
*/
|
||||
ILogToken overrideFilter(String newFilter);
|
||||
|
||||
/**
|
||||
* Returns existing filter
|
||||
*
|
||||
* @return existing filter
|
||||
*/
|
||||
String filter();
|
||||
|
||||
/**
|
||||
* Ends the capture which was started through {@link Snippet#startCapture()}.
|
||||
*
|
||||
* @param message Custom message if required
|
||||
*/
|
||||
ExecutionContext endCapture(String message);
|
||||
|
||||
/**
|
||||
* Returns the id of the thread that asked for this log token.
|
||||
*
|
||||
* @return Thread ID.
|
||||
*/
|
||||
long creatorThreadId();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
boolean isThreadLockEnabled();
|
||||
|
||||
ILogToken enableThreadLock();
|
||||
|
||||
void setCreatorThreadId(long threadId);
|
||||
|
||||
void reset();
|
||||
|
||||
void addSplit();
|
||||
|
||||
void addSplit(String message);
|
||||
|
||||
void setState(LogTokenState state);
|
||||
|
||||
LogTokenState getState();
|
||||
|
||||
/**
|
||||
* Ends the capture which was started through {@link Snippet#startCapture()}.
|
||||
*/
|
||||
ExecutionContext endCapture();
|
||||
|
||||
long getStart();
|
||||
|
||||
long getEnd();
|
||||
|
||||
void setStart(long start);
|
||||
|
||||
void setEnd(long start);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright © Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
package com.microsoft.snippet.token;
|
||||
|
||||
public enum LogTokenState {
|
||||
|
||||
ACTIVE(1),
|
||||
|
||||
END_CAPTURE_EXECUTED(2),
|
||||
|
||||
IN_POOL(3),
|
||||
|
||||
ATTENUATED(4);
|
||||
|
||||
private final int mId;
|
||||
|
||||
LogTokenState(int id) {
|
||||
this.mId = id;
|
||||
}
|
||||
|
||||
public int getID() {
|
||||
return mId;
|
||||
}
|
||||
}
|