├── .gitignore
├── .idea
├── artifacts
│ ├── benchart_js_1_0_0_beta01.xml
│ ├── benchart_js_1_0_0_rc01.xml
│ ├── benchart_jvm_1_0_0_beta01.xml
│ └── benchart_jvm_1_0_0_rc01.xml
├── benchart.iml
├── compiler.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── misc.xml
├── modules.xml
├── modules
│ ├── benchart.commonMain.iml
│ ├── benchart.commonTest.iml
│ ├── benchart.jsMain.iml
│ ├── benchart.jsTest.iml
│ ├── benchart.jvmMain.iml
│ └── benchart.jvmTest.iml
└── vcs.xml
├── .kotlin
└── metadata
│ └── kotlinTransformedMetadataLibraries
│ └── org.jetbrains.kotlin-kotlin-stdlib-2.0.20-commonMain-WPEnbA.klib
├── README.md
├── build.gradle.kts
├── cover.jpeg
├── demo.gif
├── dist
├── benchart.js
├── benchart.js.LICENSE.txt
├── benchart.js.map
├── beta
│ ├── benchart.js
│ ├── benchart.js.LICENSE.txt
│ ├── benchart.js.map
│ └── index.html
├── icons
│ ├── about.txt
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── site.webmanifest
└── index.html
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── kotlin-js-store
└── yarn.lock
├── productionExecutable
├── benchart.js
├── benchart.js.LICENSE.txt
├── benchart.js.map
├── icons
│ ├── about.txt
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── site.webmanifest
└── index.html
├── settings.gradle.kts
└── src
├── commonMain
└── kotlin
│ ├── core
│ ├── BenchmarkResult.kt
│ ├── ChartsTransformers.kt
│ └── TextNumberLine.kt
│ └── model
│ ├── Chart.kt
│ ├── FormData.kt
│ └── SavedBenchmark.kt
├── jsMain
├── kotlin
│ ├── Utils.kt
│ ├── chartjs
│ │ ├── Chart_js.kt
│ │ └── Type.kt
│ ├── components
│ │ ├── AutoFormUi.kt
│ │ ├── AutoGroupToggle.kt
│ │ ├── ChartUi.kt
│ │ ├── EditableTitle.kt
│ │ ├── Error.kt
│ │ ├── FocusGroups.kt
│ │ ├── Heading.kt
│ │ ├── SavedBenchmarkNode.kt
│ │ ├── SavedBenchmarksDropDown.kt
│ │ ├── StandardDeviationUi.kt
│ │ ├── Summary.kt
│ │ ├── TestNameDetectionToggle.kt
│ │ └── TestNames.kt
│ ├── main.kt
│ ├── page
│ │ └── home
│ │ │ ├── HomePage.kt
│ │ │ ├── HomeViewModel.kt
│ │ │ ├── ShareAwareModal.kt
│ │ │ └── SharedModal.kt
│ ├── repo
│ │ ├── BenchmarkRepo.kt
│ │ ├── FormRepo.kt
│ │ ├── GoogleFormRepo.kt
│ │ ├── GoogleSheetRepo.kt
│ │ └── UserRepo.kt
│ └── utils
│ │ ├── DefaultValues.kt
│ │ ├── JsonUtils.kt
│ │ ├── Math.kt
│ │ ├── RandomString.kt
│ │ └── SummaryUtils.kt
└── resources
│ ├── icons
│ ├── about.txt
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── site.webmanifest
│ └── index.html
├── jsTest
└── kotlin
│ ├── GoogleFormRepoTest.kt
│ └── GoogleSheetRepoTest.kt
├── jvmMain
└── kotlin
│ ├── DemoDataGen.kt
│ └── Test.kt
└── jvmTest
└── kotlin
├── BenchmarkParseTest.kt
└── core
└── TextNumberLineTest.kt
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,intellij,gradle
4 |
5 | ### Intellij ###
6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8 |
9 | # User-specific stuff
10 | .idea/**/workspace.xml
11 | .idea/**/tasks.xml
12 | .idea/**/usage.statistics.xml
13 | .idea/**/dictionaries
14 | .idea/**/shelf
15 |
16 | # Generated files
17 | .idea/**/contentModel.xml
18 |
19 | # Sensitive or high-churn files
20 | .idea/**/dataSources/
21 | .idea/**/dataSources.ids
22 | .idea/**/dataSources.local.xml
23 | .idea/**/sqlDataSources.xml
24 | .idea/**/dynamic.xml
25 | .idea/**/uiDesigner.xml
26 | .idea/**/dbnavigator.xml
27 |
28 | # Gradle
29 | .idea/**/gradle.xml
30 | .idea/**/libraries
31 |
32 | # Gradle and Maven with auto-import
33 | # When using Gradle or Maven with auto-import, you should exclude module files,
34 | # since they will be recreated, and may cause churn. Uncomment if using
35 | # auto-import.
36 | # .idea/artifacts
37 | # .idea/compiler.xml
38 | # .idea/jarRepositories.xml
39 | # .idea/modules.xml
40 | # .idea/*.iml
41 | # .idea/modules
42 | # *.iml
43 | # *.ipr
44 |
45 | # CMake
46 | cmake-build-*/
47 |
48 | # Mongo Explorer plugin
49 | .idea/**/mongoSettings.xml
50 |
51 | # File-based project format
52 | *.iws
53 |
54 | # IntelliJ
55 | out/
56 |
57 | # mpeltonen/sbt-idea plugin
58 | .idea_modules/
59 |
60 | # JIRA plugin
61 | atlassian-ide-plugin.xml
62 |
63 | # Cursive Clojure plugin
64 | .idea/replstate.xml
65 |
66 | # Crashlytics plugin (for Android Studio and IntelliJ)
67 | com_crashlytics_export_strings.xml
68 | crashlytics.properties
69 | crashlytics-build.properties
70 | fabric.properties
71 |
72 | # Editor-based Rest Client
73 | .idea/httpRequests
74 |
75 | # Android studio 3.1+ serialized cache file
76 | .idea/caches/build_file_checksums.ser
77 |
78 | ### Intellij Patch ###
79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
80 |
81 | # *.iml
82 | # modules.xml
83 | # .idea/misc.xml
84 | # *.ipr
85 |
86 | # Sonarlint plugin
87 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
88 | .idea/**/sonarlint/
89 |
90 | # SonarQube Plugin
91 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
92 | .idea/**/sonarIssues.xml
93 |
94 | # Markdown Navigator plugin
95 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
96 | .idea/**/markdown-navigator.xml
97 | .idea/**/markdown-navigator-enh.xml
98 | .idea/**/markdown-navigator/
99 |
100 | .DS_Store
101 |
102 | # Cache file creation bug
103 | # See https://youtrack.jetbrains.com/issue/JBR-2257
104 | .idea/$CACHE_FILE$
105 |
106 | # CodeStream plugin
107 | # https://plugins.jetbrains.com/plugin/12206-codestream
108 | .idea/codestream.xml
109 |
110 | ### Kotlin ###
111 | # Compiled class file
112 | *.class
113 |
114 | # Log file
115 | *.log
116 |
117 | # BlueJ files
118 | *.ctxt
119 |
120 | # Mobile Tools for Java (J2ME)
121 | .mtj.tmp/
122 |
123 | # Package Files #
124 | *.jar
125 | *.war
126 | *.nar
127 | *.ear
128 | *.zip
129 | *.tar.gz
130 | *.rar
131 |
132 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
133 | hs_err_pid*
134 |
135 | ### Gradle ###
136 | .gradle
137 | build/
138 |
139 | # Ignore Gradle GUI config
140 | gradle-app.setting
141 |
142 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
143 | !gradle-wrapper.jar
144 |
145 | # Cache of project
146 | .gradletasknamecache
147 |
148 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
149 | # gradle/wrapper/gradle-wrapper.properties
150 |
151 | ### Gradle Patch ###
152 | **/build/
153 |
154 | # End of https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle
155 | build
156 | gpm.json
157 |
158 |
159 |
--------------------------------------------------------------------------------
/.idea/artifacts/benchart_js_1_0_0_beta01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/benchart_js_1_0_0_rc01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/benchart_jvm_1_0_0_beta01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/artifacts/benchart_jvm_1_0_0_rc01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/build/libs
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/benchart.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/modules/benchart.commonMain.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | SOURCE_SET_HOLDER
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compose-compiler-plugin-embeddable/2.0.20/d3b5072df7943425b2ec5b5cfb323701cb5d8bd2/kotlin-compose-compiler-plugin-embeddable-2.0.20.jar
20 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin-embeddable/2.0.20/c48d1e6a5daf2345b982d68de4f259e0a1a8a449/kotlin-serialization-compiler-plugin-embeddable-2.0.20.jar
21 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-jvm/2.0.20/46b8d2d2028448f6596cc3ce0ad7b10b259ec236/kotlin-scripting-jvm-2.0.20.jar
22 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-common/2.0.20/af6097b8ac359457ec1cf9b34bd9b5313b52eb6/kotlin-scripting-common-2.0.20.jar
23 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.0.20/7388d355f7cceb002cd387ccb7ab3850e4e0a07f/kotlin-stdlib-2.0.20.jar
24 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar
25 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/2.0.20/4aea042b39014e0a924c2e1d4a21b6fff7e4d35/kotlin-script-runtime-2.0.20.jar
26 |
27 |
28 |
29 |
30 | plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaClasses=false
31 | plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=false
32 | plugin:androidx.compose.compiler.plugins.kotlin:traceMarkersEnabled=true
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.idea/modules/benchart.commonTest.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | benchart:commonMain
7 | SOURCE_SET_HOLDER
8 |
9 | jsNodeTest|:|js|js
10 | jsBrowserTest|:|js|js
11 | jvmTest|:|jvm|jvm
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compose-compiler-plugin-embeddable/2.0.20/d3b5072df7943425b2ec5b5cfb323701cb5d8bd2/kotlin-compose-compiler-plugin-embeddable-2.0.20.jar
26 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin-embeddable/2.0.20/c48d1e6a5daf2345b982d68de4f259e0a1a8a449/kotlin-serialization-compiler-plugin-embeddable-2.0.20.jar
27 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-jvm/2.0.20/46b8d2d2028448f6596cc3ce0ad7b10b259ec236/kotlin-scripting-jvm-2.0.20.jar
28 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-common/2.0.20/af6097b8ac359457ec1cf9b34bd9b5313b52eb6/kotlin-scripting-common-2.0.20.jar
29 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.0.20/7388d355f7cceb002cd387ccb7ab3850e4e0a07f/kotlin-stdlib-2.0.20.jar
30 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar
31 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/2.0.20/4aea042b39014e0a924c2e1d4a21b6fff7e4d35/kotlin-script-runtime-2.0.20.jar
32 |
33 |
34 |
35 |
36 | plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaClasses=false
37 | plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=false
38 | plugin:androidx.compose.compiler.plugins.kotlin:traceMarkersEnabled=true
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/.idea/modules/benchart.jsMain.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | benchart:commonMain
7 |
8 | benchart.commonMain
9 |
10 | COMPILATION_AND_SOURCE_SET_HOLDER
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | jsMain:commonMain
32 |
33 |
34 |
35 | jsMain
36 | commonMain
37 |
38 |
39 |
40 |
41 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compose-compiler-plugin-embeddable/2.0.20/d3b5072df7943425b2ec5b5cfb323701cb5d8bd2/kotlin-compose-compiler-plugin-embeddable-2.0.20.jar
42 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin-embeddable/2.0.20/c48d1e6a5daf2345b982d68de4f259e0a1a8a449/kotlin-serialization-compiler-plugin-embeddable-2.0.20.jar
43 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-jvm/2.0.20/46b8d2d2028448f6596cc3ce0ad7b10b259ec236/kotlin-scripting-jvm-2.0.20.jar
44 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-common/2.0.20/af6097b8ac359457ec1cf9b34bd9b5313b52eb6/kotlin-scripting-common-2.0.20.jar
45 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.0.20/7388d355f7cceb002cd387ccb7ab3850e4e0a07f/kotlin-stdlib-2.0.20.jar
46 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar
47 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/2.0.20/4aea042b39014e0a924c2e1d4a21b6fff7e4d35/kotlin-script-runtime-2.0.20.jar
48 |
49 |
50 |
51 |
52 | plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaClasses=false
53 | plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=false
54 | plugin:androidx.compose.compiler.plugins.kotlin:traceMarkersEnabled=true
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/.idea/modules/benchart.jsTest.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | benchart:commonTest
7 |
8 | benchart:jsMain
9 | benchart:commonMain
10 |
11 |
12 | benchart.commonTest
13 |
14 | COMPILATION_AND_SOURCE_SET_HOLDER
15 |
16 | jsNodeTest|:|js|js
17 | jsBrowserTest|:|js|js
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | jsTest:commonTest
40 |
41 |
42 |
43 | jsTest
44 | commonTest
45 |
46 |
47 |
48 |
49 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compose-compiler-plugin-embeddable/2.0.20/d3b5072df7943425b2ec5b5cfb323701cb5d8bd2/kotlin-compose-compiler-plugin-embeddable-2.0.20.jar
50 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin-embeddable/2.0.20/c48d1e6a5daf2345b982d68de4f259e0a1a8a449/kotlin-serialization-compiler-plugin-embeddable-2.0.20.jar
51 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-jvm/2.0.20/46b8d2d2028448f6596cc3ce0ad7b10b259ec236/kotlin-scripting-jvm-2.0.20.jar
52 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-common/2.0.20/af6097b8ac359457ec1cf9b34bd9b5313b52eb6/kotlin-scripting-common-2.0.20.jar
53 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.0.20/7388d355f7cceb002cd387ccb7ab3850e4e0a07f/kotlin-stdlib-2.0.20.jar
54 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar
55 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/2.0.20/4aea042b39014e0a924c2e1d4a21b6fff7e4d35/kotlin-script-runtime-2.0.20.jar
56 |
57 |
58 |
59 |
60 | plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaClasses=false
61 | plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=false
62 | plugin:androidx.compose.compiler.plugins.kotlin:traceMarkersEnabled=true
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/.idea/modules/benchart.jvmMain.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | benchart:commonMain
7 |
8 | benchart.commonMain
9 |
10 | COMPILATION_AND_SOURCE_SET_HOLDER
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | jvmMain:commonMain
29 |
30 |
31 |
32 | jvmMain
33 | commonMain
34 |
35 |
36 |
37 |
38 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compose-compiler-plugin-embeddable/2.0.20/d3b5072df7943425b2ec5b5cfb323701cb5d8bd2/kotlin-compose-compiler-plugin-embeddable-2.0.20.jar
39 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin-embeddable/2.0.20/c48d1e6a5daf2345b982d68de4f259e0a1a8a449/kotlin-serialization-compiler-plugin-embeddable-2.0.20.jar
40 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-jvm/2.0.20/46b8d2d2028448f6596cc3ce0ad7b10b259ec236/kotlin-scripting-jvm-2.0.20.jar
41 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-common/2.0.20/af6097b8ac359457ec1cf9b34bd9b5313b52eb6/kotlin-scripting-common-2.0.20.jar
42 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.0.20/7388d355f7cceb002cd387ccb7ab3850e4e0a07f/kotlin-stdlib-2.0.20.jar
43 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar
44 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/2.0.20/4aea042b39014e0a924c2e1d4a21b6fff7e4d35/kotlin-script-runtime-2.0.20.jar
45 |
46 |
47 |
48 |
49 | plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaClasses=false
50 | plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=false
51 | plugin:androidx.compose.compiler.plugins.kotlin:traceMarkersEnabled=true
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/.idea/modules/benchart.jvmTest.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | benchart:commonTest
7 |
8 | benchart:jvmMain
9 | benchart:commonMain
10 |
11 |
12 | benchart.commonTest
13 |
14 | COMPILATION_AND_SOURCE_SET_HOLDER
15 |
16 | jvmTest|:|jvm|jvm
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | jvmTest:commonTest
36 |
37 |
38 |
39 | jvmTest
40 | commonTest
41 |
42 |
43 |
44 |
45 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compose-compiler-plugin-embeddable/2.0.20/d3b5072df7943425b2ec5b5cfb323701cb5d8bd2/kotlin-compose-compiler-plugin-embeddable-2.0.20.jar
46 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin-embeddable/2.0.20/c48d1e6a5daf2345b982d68de4f259e0a1a8a449/kotlin-serialization-compiler-plugin-embeddable-2.0.20.jar
47 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-jvm/2.0.20/46b8d2d2028448f6596cc3ce0ad7b10b259ec236/kotlin-scripting-jvm-2.0.20.jar
48 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-scripting-common/2.0.20/af6097b8ac359457ec1cf9b34bd9b5313b52eb6/kotlin-scripting-common-2.0.20.jar
49 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.0.20/7388d355f7cceb002cd387ccb7ab3850e4e0a07f/kotlin-stdlib-2.0.20.jar
50 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar
51 | $USER_HOME$/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-script-runtime/2.0.20/4aea042b39014e0a924c2e1d4a21b6fff7e4d35/kotlin-script-runtime-2.0.20.jar
52 |
53 |
54 |
55 |
56 | plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaClasses=false
57 | plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=false
58 | plugin:androidx.compose.compiler.plugins.kotlin:traceMarkersEnabled=true
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-stdlib-2.0.20-commonMain-WPEnbA.klib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-stdlib-2.0.20-commonMain-WPEnbA.klib
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # 📊 benchart
4 |
5 | > A web tool to visualize and compare plain text data with Android Macrobenchmark data support
6 |
7 | 
8 |
9 |
10 |
11 |
12 |
13 | 
14 |
15 | > Yeah, correct, [he](https://en.wikipedia.org/wiki/Virat_Kohli)'s saying "Benchart"
16 |
17 | ## ✨ Demo
18 |
19 | https://user-images.githubusercontent.com/9678279/204081186-36c7ce90-6c46-4ec6-9c36-dd16cdf25acf.mov
20 |
21 | ## 🐣 Getting Started
22 |
23 | Let's start with some sample log data to learn the basics.
24 |
25 | 
26 |
27 | ### 📄 Input
28 |
29 | - Benchart accepts input as plain text.
30 | - A block is a chunk of information that you can compare.
31 | - Blocks are separated by a blank line.
32 | - The first line of the block will be treated as the header line.
33 | - In the above example, `# before` and `# after`.
34 | - Remaining lines will be treated as input lines.
35 | - In an input line, the last number is treated as the value, and anything before that as the key.
36 | - For example, given the input line `myFirstFunction() = 90ms`, the key will be `myFirstFunction`, and the value will be `90`.
37 | - All special characters will be stripped out from all the lines (e.g., `(`, `)`, `=`, `#`).
38 |
39 | ### 📊 Visualization
40 |
41 | - The X-axis of the chart is for keys.
42 | - The Y-axis of the chart is for values.
43 | - Headers will be given as legend.
44 |
45 | ### 💻 Result
46 |
47 | - The result compares the blocks and shows 2 states:
48 | - `better`: a decrease in value.
49 | - `worse`: an increase in value.
50 | - Each result line shows the change in percentage as well as the change in value.
51 |
52 | ## 🔃 The Swap
53 |
54 | You can swap the blocks, and the results will also be swapped (see the above example to find the difference).
55 |
56 |
57 |
58 | ## 🧱 Multi-blocks
59 |
60 | You can have `n` number of blocks. The first word of the header will be considered a group. When you have multiple blocks starting with the same word, the result will be the average of those blocks. See the example below.
61 |
62 | 
63 |
64 | ## 👥 Auto Group
65 |
66 | To color the same group with the same color on the chart, enable Auto Group.
67 |
68 | 
69 |
70 | ## ➗ Auto Average
71 |
72 | In a block, if there are multiple lines with the same key, they will be averaged.
73 |
74 | 
75 |
76 | ## 🎯 Focus Group
77 |
78 | You'll see a new element called Focus Group when auto average is performed.
79 |
80 | 
81 |
82 | Selecting a group from the Focus Group dropdown will show each value in the chart.
83 |
84 | 
85 |
86 | ## 🤖 Android Support
87 |
88 | You can paste your Macrobenchmark result data into the input box, and it'll draw custom charts for each metric.
89 |
90 | 
91 |
92 | ## ⭐️ Star History
93 |
94 | [](https://star-history.com/#theapache64/benchart&Date)
95 |
96 | ## ✍️ Author
97 |
98 | 👤 **theapache64**
99 |
100 | * Twitter: @theapache64
101 | * Email: theapache64@gmail.com
102 |
103 | Feel free to ping me 😉
104 |
105 | ## 🤝 Contributing
106 |
107 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
108 |
109 | 1. Open an issue first to discuss what you would like to change.
110 | 1. Fork the project.
111 | 1. Create your feature branch (`git checkout -b feature/amazing-feature`).
112 | 1. Commit your changes (`git commit -m 'Add some amazing feature'`).
113 | 1. Push to the branch (`git push origin feature/amazing-feature`).
114 | 1. Open a pull request.
115 |
116 | Please make sure to update tests as appropriate.
117 |
118 | ## ❤ Show Your Support
119 |
120 | Give a ⭐️ if this project helped you!
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | ## 📝 License
131 | ```
132 | Copyright © 2024 - theapache64
133 |
134 | Licensed under the Apache License, Version 2.0 (the "License");
135 | you may not use this file except in compliance with the License.
136 | You may obtain a copy of the License at
137 |
138 | http://www.apache.org/licenses/LICENSE-2.0
139 |
140 | Unless required by applicable law or agreed to in writing, software
141 | distributed under the License is distributed on an "AS IS" BASIS,
142 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
143 | See the License for the specific language governing permissions and
144 | limitations under the License.
145 | ```
146 |
147 | _This README was generated by [readgen](https://github.com/theapache64/readgen)_ ❤
148 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Add compose gradle plugin
2 | plugins {
3 | kotlin("multiplatform") version "2.0.20"
4 | id("org.jetbrains.kotlin.plugin.compose") version "2.0.20"
5 | id("org.jetbrains.compose") version "1.7.0-alpha03"
6 | kotlin("plugin.serialization") version "2.0.20"
7 | }
8 | group = "com.theapache64.benchart"
9 | version = "1.0.0-rc01"
10 |
11 | // Add maven repositories
12 | repositories {
13 | mavenCentral()
14 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
15 | }
16 |
17 | kotlin {
18 | jvm()
19 | js(IR) {
20 | nodejs {
21 | testTask {
22 | useMocha {
23 | timeout = "9s"
24 | }
25 | }
26 | }
27 | browser {
28 | testTask {
29 | useKarma {
30 | useChromeHeadless()
31 | }
32 | }
33 | }
34 | binaries.executable()
35 | }
36 | sourceSets {
37 | val jsMain by getting {
38 | dependencies {
39 | implementation(compose.web.core)
40 | implementation(compose.runtime)
41 | implementation(npm("chart.js", "4.4.7", generateExternals = false))
42 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
43 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC.2")
44 |
45 | }
46 | }
47 |
48 | val jsTest by getting {
49 | dependencies {
50 | implementation("org.jetbrains.kotlin:kotlin-test-js")
51 | }
52 | }
53 |
54 | val jvmMain by getting {
55 | dependencies {
56 | implementation(compose.runtime)
57 | }
58 | }
59 |
60 | val jvmTest by getting {
61 | dependencies {
62 | implementation("junit:junit:4.13.2")
63 | }
64 | }
65 | }
66 | }
67 | // Workaround for https://youtrack.jetbrains.com/issue/KT-49124
68 | rootProject.extensions.configure {
69 | versions.webpackCli.version = "4.10.0"
70 | }
71 |
--------------------------------------------------------------------------------
/cover.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/cover.jpeg
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/demo.gif
--------------------------------------------------------------------------------
/dist/benchart.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * @kurkle/color v0.3.4
3 | * https://github.com/kurkle/color#readme
4 | * (c) 2024 Jukka Kurkela
5 | * Released under the MIT License
6 | */
7 |
8 | /*!
9 | * Chart.js v4.4.7
10 | * https://www.chartjs.org
11 | * (c) 2024 Chart.js Contributors
12 | * Released under the MIT License
13 | */
14 |
--------------------------------------------------------------------------------
/dist/beta/benchart.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * @kurkle/color v0.2.1
3 | * https://github.com/kurkle/color#readme
4 | * (c) 2022 Jukka Kurkela
5 | * Released under the MIT License
6 | */
7 |
8 | /*!
9 | * Chart.js v3.9.1
10 | * https://www.chartjs.org
11 | * (c) 2022 Chart.js Contributors
12 | * Released under the MIT License
13 | */
14 |
--------------------------------------------------------------------------------
/dist/beta/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 | 📊 benchart
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
--------------------------------------------------------------------------------
/dist/icons/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following graphics from Twitter Twemoji:
2 |
3 | - Graphics Title: 1f4ca.svg
4 | - Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
5 | - Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f4ca.svg
6 | - Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
7 |
--------------------------------------------------------------------------------
/dist/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/dist/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/dist/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/dist/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/dist/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/dist/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/dist/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/dist/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/dist/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/dist/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/dist/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/dist/icons/favicon.ico
--------------------------------------------------------------------------------
/dist/icons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 | 📊 benchart
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 |
36 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/productionExecutable/benchart.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * @kurkle/color v0.2.1
3 | * https://github.com/kurkle/color#readme
4 | * (c) 2022 Jukka Kurkela
5 | * Released under the MIT License
6 | */
7 |
8 | /*!
9 | * Chart.js v3.9.1
10 | * https://www.chartjs.org
11 | * (c) 2022 Chart.js Contributors
12 | * Released under the MIT License
13 | */
14 |
--------------------------------------------------------------------------------
/productionExecutable/icons/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following graphics from Twitter Twemoji:
2 |
3 | - Graphics Title: 1f4ca.svg
4 | - Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
5 | - Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f4ca.svg
6 | - Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
7 |
--------------------------------------------------------------------------------
/productionExecutable/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/productionExecutable/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/productionExecutable/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/productionExecutable/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/productionExecutable/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/productionExecutable/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/productionExecutable/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/productionExecutable/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/productionExecutable/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/productionExecutable/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/productionExecutable/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/productionExecutable/icons/favicon.ico
--------------------------------------------------------------------------------
/productionExecutable/icons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/productionExecutable/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 | 📊 benchart
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
5 | }
6 | }
7 | rootProject.name = "benchart"
8 |
9 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/core/BenchmarkResult.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import model.FormData
4 |
5 | open class InvalidDataException(message: String?) : Throwable(message)
6 | class InvalidBenchmarkDataException(message: String?) : InvalidDataException(message)
7 | class InvalidGenericDataException(message: String?) : InvalidDataException(message)
8 |
9 | enum class MetricUnit(val singular: String, val plural: String) {
10 | View(" view", " views"),
11 | Ms(singular = "ms", plural = "ms"),
12 | Mah(singular = " Mah", plural = "Mah"),
13 | Kb(singular = "kb", plural = "kb"),
14 | Frame(" frame", " frames"),
15 | Percentage("%", "%")
16 | }
17 |
18 | data class BlockRow(
19 | val title: String,
20 | val fullData: Map>
21 | ) {
22 | val avgData: Map = fullData.mapValues { it.value.average().toFloat() }
23 | }
24 |
25 | data class SupportedMetrics(
26 | val key: String,
27 | val emoji: String,
28 | val title: String
29 | ) {
30 | /*Duration(
31 | emoji = "⏱",
32 | key = "frameDurationCpuMs",
33 | title = "Duration Summary"
34 | ),
35 | Overrun(
36 | emoji = "🏃🏻♂️",
37 | key = "frameOverrunMs",
38 | title = "Overrun Summary"
39 | ),
40 | InitialDisplay(
41 | emoji = "🌘",
42 | key = "timeToInitialDisplayMs",
43 | title = "Initial Display Summary"
44 | ),
45 | FullDisplay(
46 | emoji = "🌕",
47 | key = "timeToFullDisplayMs",
48 | title = "Full Display Summary"
49 | ),
50 | CreateViewCount(
51 | emoji = "🔢",
52 | key = "RV CreateViewCount",
53 | title = "Create View Count"
54 | ),
55 | CreateViewSum(
56 | emoji = "⏲",
57 | key = "RV CreateViewSumMs",
58 | title = "Create View Time Sum"
59 | ),
60 | BindViewCount(
61 | emoji = "🔄",
62 | key = "RV OnBindViewCount",
63 | title = "Bind View Count"
64 | ),
65 | BindViewSum(
66 | emoji = "⌛️",
67 | key = "RV OnBindViewSumMs",
68 | title = "Bind View Time Sum"
69 | ),
70 | FrameCount(
71 | emoji = "🖼",
72 | key = "frameCount",
73 | title = "Frame Count"
74 | ),
75 | JankPercent(
76 | emoji = "📊",
77 | key = "gfxFrameJankPercent",
78 | title = "Frame Jank Percentage"
79 | ),
80 | FrameTime50(
81 | emoji = "⚡️",
82 | key = "gfxFrameTime50thPercentileMs",
83 | title = "Frame Time 50th Percentile"
84 | ),
85 | FrameTime90(
86 | emoji = "🚀",
87 | key = "gfxFrameTime90thPercentileMs",
88 | title = "Frame Time 90th Percentile"
89 | ),
90 | FrameTime95(
91 | emoji = "🎯",
92 | key = "gfxFrameTime95thPercentileMs",
93 | title = "Frame Time 95th Percentile"
94 | ),
95 | FrameTime99(
96 | emoji = "⚠️",
97 | key = "gfxFrameTime99thPercentileMs",
98 | title = "Frame Time 99th Percentile"
99 | ),
100 | GfxFrameCount(
101 | emoji = "🎬",
102 | key = "gfxFrameTotalCount",
103 | title = "GFX Frame Total Count"
104 | ),
105 | MemoryHeap(
106 | emoji = "💾",
107 | key = "memoryHeapSizeMaxKb",
108 | title = "Memory Heap Size"
109 | ),
110 | OrderListPopulationCount(
111 | emoji = "📋",
112 | key = "order_list_populationCount",
113 | title = "Order List Population Count"
114 | ),
115 | OrderListPopulationSum(
116 | emoji = "📝",
117 | key = "order_list_populationSumMs",
118 | title = "Order List Population Time Sum"
119 | )*/
120 | }
121 |
122 | enum class InputType {
123 | GENERIC,
124 | MACRO_BENCHMARK
125 | }
126 |
127 | data class ResultContainer(
128 | val inputType: InputType,
129 | val benchmarkResults: List,
130 | val focusGroups: Set
131 | )
132 |
133 | data class BenchmarkResult(
134 | val title: String,
135 | val testName: String?,
136 | val blockRows: List
137 | ) {
138 | companion object {
139 | const val FOCUS_GROUP_ALL = "All"
140 |
141 | private val titleStripRegEx = "\\W+".toRegex()
142 | private val genericTitleStripRegEx = "\\W+".toRegex()
143 | private val testNameRegex = "[A-Z].*_[a-z].*".toRegex()
144 |
145 | fun parse(form: FormData, focusGroup: String): ResultContainer? {
146 |
147 | val blocks = form.data
148 | .split("\n").joinToString(separator = "\n") { it.trim() }
149 | .split("^\\s+".toRegex(RegexOption.MULTILINE)).map { it.trim() }
150 | .filter { it.isNotBlank() }
151 |
152 | println("parsing input...")
153 | if (blocks.isEmpty()) return null
154 | if (form.isGenericInput()) return parseGenericInput(blocks, focusGroup)
155 |
156 | println("parsing machine generated benchmark input...")
157 | val benchmarkResults = mutableListOf()
158 |
159 | for ((index, block) in blocks.withIndex()) {
160 | println("block: '$block'")
161 | val lines = block.split("\n").map { it.trim() }
162 | var title: String? = null
163 | var testName: String? = null
164 | val blockRows = mutableListOf()
165 | for (line in lines) {
166 |
167 | if (title == null && isHumanLine(line)) {
168 | title = line
169 | }
170 |
171 | if (form.isTestNameDetectionEnabled && isTestName(line)) {
172 | if (testName != null && blockRows.isNotEmpty()) {
173 |
174 | if (title == null) {
175 | title = "benchmark $index $testName"
176 | }
177 |
178 | // We already have an unsaved testData, so let's save it
179 | benchmarkResults.add(
180 | BenchmarkResult(
181 | title = title,
182 | testName = testName,
183 | blockRows = blockRows
184 | )
185 | )
186 |
187 | blockRows.clear()
188 | }
189 |
190 | testName = line
191 | }
192 |
193 | val metricName = findMetricKeyOrNull(line)
194 | println("QuickTag: BenchmarkResult:parse: metric name is $metricName")
195 | if (metricName != null) {
196 | val isMetricAlreadyAdded = blockRows.find { it.title == metricName } != null
197 | if (isMetricAlreadyAdded) {
198 | throw InvalidBenchmarkDataException("Two $metricName found in block ${index + 1}. Expected only one")
199 | }
200 |
201 | blockRows.add(
202 | BlockRow(
203 | title = metricName,
204 | fullData = parseValues(metricName, line).map { (key, value) ->
205 | key to listOf(value)
206 | }.toMap()
207 | )
208 | )
209 | }
210 | }
211 |
212 | if (title == null) {
213 | title = "benchmark $index"
214 | }
215 |
216 | title = parseTitle(title)
217 |
218 | if (blockRows.isNotEmpty()) {
219 | benchmarkResults.add(
220 | BenchmarkResult(
221 | title = title,
222 | testName = testName,
223 | blockRows = blockRows
224 | )
225 | )
226 | }
227 | }
228 |
229 | return ResultContainer(InputType.MACRO_BENCHMARK, benchmarkResults, setOf(FOCUS_GROUP_ALL))
230 | }
231 |
232 | private fun parseGenericInput(
233 | blocks: List,
234 | focusGroup: String
235 | ): ResultContainer {
236 | val (focusGroups, benchmarkResults) = parseMultiLineGenericInput(blocks, focusGroup)
237 | return ResultContainer(
238 | InputType.GENERIC,
239 | benchmarkResults,
240 | focusGroups
241 | )
242 | }
243 |
244 | private fun createChartTitle(blockRows: MutableList): String {
245 | return blockRows.joinToString(separator = " vs ") { it.title }
246 | }
247 |
248 | private fun parseMultiLineGenericInput(
249 | blocks: List,
250 | focusGroup: String
251 | ): Pair, List> {
252 | val benchmarkResults = mutableListOf()
253 | val blockRows = mutableListOf()
254 | val focusGroups = mutableSetOf(FOCUS_GROUP_ALL)
255 | for ((index, block) in blocks.withIndex()) {
256 | val lines = block.split("\n").map { it.trim() }
257 | var title: String? = null
258 | val valuesMap = mutableMapOf>()
259 | for ((lineIndex, line) in lines.withIndex()) {
260 |
261 | if (title == null && isHumanLine(line)) {
262 | title = line
263 | continue
264 | }
265 |
266 | if (line.shouldSkip()) {
267 | continue
268 | }
269 |
270 | val textNumberLine = TextNumberLine.parse(lineIndex, line) ?: continue
271 | val genericTitle = parseGenericTitle(textNumberLine.text)
272 | valuesMap.getOrPut(genericTitle) { mutableListOf() }.add(textNumberLine.number)
273 | }
274 |
275 | if (title == null) {
276 | title = "benchmark $index"
277 | }
278 |
279 | title = parseGenericTitle(title)
280 |
281 | blockRows.add(
282 | BlockRow(
283 | title = title,
284 | fullData = valuesMap
285 | )
286 | )
287 | }
288 |
289 | for (blockRow in blockRows) {
290 | for ((key, value) in blockRow.fullData) {
291 | if (value.size > 1) {
292 | focusGroups.add(key)
293 | }
294 | }
295 | }
296 |
297 | checkDataIntegrity(blockRows)
298 |
299 | val chartTitle = createChartTitle(blockRows)
300 |
301 | benchmarkResults.add(
302 | BenchmarkResult(
303 | title = chartTitle,
304 | testName = null,
305 | blockRows = blockRows
306 | )
307 | )
308 |
309 | return if (focusGroup == FOCUS_GROUP_ALL || focusGroup !in focusGroups) {
310 | Pair(focusGroups, benchmarkResults)
311 | } else {
312 | Pair(focusGroups, focus(benchmarkResults, focusGroup))
313 | }
314 | }
315 |
316 | private fun focus(benchmarkResults: List, focusGroup: String): List {
317 | val newBenchmarkResult = mutableListOf()
318 | for (result in benchmarkResults) {
319 | val blockRows = mutableListOf()
320 | for (blockRow in result.blockRows) {
321 | blockRows.add(
322 | BlockRow(
323 | title = blockRow.title,
324 | fullData = blockRow.fullData[focusGroup]?.mapIndexed { index, value ->
325 | Pair(getPositionText(index + 1), listOf(value))
326 | }?.toMap() ?: error("Invalid focus group '$focusGroup' for ${blockRow.title}")
327 | )
328 | )
329 | }
330 | newBenchmarkResult.add(
331 | BenchmarkResult(
332 | title = "$focusGroup - ${result.title}",
333 | testName = result.testName,
334 | blockRows = blockRows
335 | )
336 | )
337 | }
338 | return newBenchmarkResult
339 | }
340 |
341 | private fun getPositionText(index: Int): String {
342 | val suffix = when {
343 | index % 100 in 11..13 -> "th"
344 | index % 10 == 1 -> "st"
345 | index % 10 == 2 -> "nd"
346 | index % 10 == 3 -> "rd"
347 | else -> "th"
348 | }
349 | return "$index$suffix"
350 | }
351 |
352 |
353 | private fun checkDataIntegrity(blockRows: List) {
354 | if (blockRows.size >= 2) {
355 | val originalValueOrder = blockRows.first().avgData.keys.toList().sorted()
356 | for ((index, blockRow) in blockRows.withIndex()) {
357 | if (index == 0) {
358 | continue
359 | }
360 | val currentValueOrder = blockRow.avgData.keys.toList().sorted()
361 | if (originalValueOrder != currentValueOrder) {
362 | error("Missing ${originalValueOrder.minus(currentValueOrder.toSet())} in '${blockRow.title}' block")
363 | }
364 | }
365 | }
366 |
367 | val keyLengthMap = mutableMapOf()
368 | blockRows.forEach { blockRow ->
369 | blockRow.fullData.forEach { (key, values) ->
370 | if (keyLengthMap.containsKey(key) && keyLengthMap[key] != values.size) {
371 | error("Item count mismatch. For '$key', ${keyLengthMap[key]} rows expected, but found ${values.size} in '${blockRow.title}' block")
372 | } else {
373 | keyLengthMap[key] = values.size
374 | }
375 | }
376 | }
377 | }
378 |
379 |
380 | private fun isTestName(line: String): Boolean {
381 | return testNameRegex.matches(line)
382 | }
383 |
384 | private fun parseTitle(title: String): String {
385 | return title
386 | .replace(titleStripRegEx, " ")
387 | .replace("\\s{2,}".toRegex(), " ")
388 | .trim()
389 | }
390 |
391 | private fun parseGenericTitle(title: String): String {
392 | return title
393 | .replace(genericTitleStripRegEx, " ")
394 | .replace("\\s{2,}".toRegex(), " ")
395 | .trim()
396 | }
397 |
398 | private fun isHumanLine(line: String): Boolean {
399 | return !isMachineLine(line)
400 | }
401 |
402 | private fun isMachineLine(line: String): Boolean {
403 | return line.trim().contains("Traces: ") || line.matches(percentileRegex) || line.matches(minMaxMedianRegex)
404 | }
405 |
406 | private fun parseValues(key: String, data: String): Map {
407 | if (!data.startsWith(key)) {
408 | error("Invalid $key.Expected to start with '$key' but found '$data'")
409 | }
410 |
411 | val transformedList = data.replace(key, "")
412 | .replace("\\s+".toRegex(), " ")
413 | .split(", ")
414 | // remove commas in numbers
415 | .map { it.replace(",", "").trim().split(" ") }
416 |
417 | val valueMap = mutableMapOf()
418 | for (item in transformedList) {
419 | valueMap[item[0]] = item[1].toFloat()
420 | }
421 | return valueMap
422 | }
423 |
424 |
425 | private fun findMetricKeyOrNull(line: String): String? {
426 | when {
427 | line.matches(minMaxMedianRegex) -> {
428 | val minMaxMedianMatch = minMaxMedianRegex.matchEntire(line)!!
429 | val (metricName, _) = minMaxMedianMatch.destructured
430 | return metricName
431 | }
432 |
433 | line.matches(percentileRegex) -> {
434 | val percentileMatch = percentileRegex.matchEntire(line)!!
435 | val (metricName, _) = percentileMatch.destructured
436 | return metricName
437 | }
438 |
439 | else -> return null
440 | }
441 | }
442 |
443 | private fun String.shouldSkip(): Boolean {
444 | return this == "startup type is: cold" || this == "startup type is: warm" || this == "startup type is: hot"
445 | }
446 | }
447 |
448 |
449 | }
450 |
451 | val minMaxMedianRegex = "^(.+?)\\s+min\\s+(.+?),\\s+median\\s+(.+?),\\s+max\\s+(.+?)\$".toRegex()
452 | val percentileRegex = "^(.+?)\\s+P50\\s+(.+?),\\s+P90\\s+(.+?),\\s+P95\\s+(.+?),\\s+P99\\s+(.+)\$".toRegex()
453 |
454 |
455 | private fun FormData.isGenericInput(): Boolean {
456 | return this.data.lines().find { line -> line.matches(minMaxMedianRegex) || line.matches(percentileRegex) } == null
457 | }
458 |
459 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/core/ChartsTransformers.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import model.Chart
4 | import model.ChartsBundle
5 |
6 |
7 | fun List.toCharts(): ChartsBundle {
8 | val chartNames = this
9 | .map { result ->
10 | result.blockRows.map { dataPoint ->
11 | dataPoint.title
12 | }
13 | }
14 | .flatten()
15 | .toSet()
16 |
17 | val charts = mutableListOf()
18 | for (chartName in chartNames) {
19 | // before1 -> {P50=40.5, P90=45.8, P95=60.4, P99=80.4}
20 | val dataSets = mutableMapOf>()
21 | for (item in this) {
22 | dataSets[item.title] = item.blockRows.find { it.title == chartName }?.avgData ?: emptyMap()
23 | }
24 |
25 | charts.add(
26 | Chart(
27 | emoji = getMetricEmoji(chartName),
28 | label = chartName, // frameDurationCpuMs, frameOverrunMs, etc
29 | dataSets = dataSets
30 | )
31 | )
32 | }
33 |
34 | val groupMap = parseGroupMap(this, isGeneric = false)
35 | return ChartsBundle(
36 | groupMap = groupMap,
37 | charts = charts
38 | )
39 | }
40 |
41 | fun getMetricEmoji(chartName: String): String {
42 | // TODO:
43 | return "📊"
44 | }
45 |
46 |
47 | fun List.toGenericChart(): ChartsBundle {
48 | // Generic chart will be always 1
49 | val result = this.first()
50 |
51 | val chart = Chart(
52 | emoji = "📊",
53 | label = result.title,
54 | dataSets = mutableMapOf>().apply {
55 | for(blockRow in result.blockRows){
56 | put(blockRow.title, blockRow.avgData)
57 | }
58 | },
59 | bsClass = "col-lg-12"
60 | )
61 |
62 | return ChartsBundle(
63 | groupMap = parseGroupMap(this, isGeneric = true),
64 | charts = listOf(
65 | chart
66 | )
67 | )
68 | }
69 |
70 |
71 | data class GroupMap(
72 | val autoGroupMap: Map,
73 | val wordColorMap: Map
74 | )
75 |
76 | fun parseGroupMap(
77 | benchmarkResults: List,
78 | isGeneric : Boolean
79 | ): GroupMap {
80 | val autoGroupMap = mutableMapOf()
81 | val titles = if(isGeneric){
82 | benchmarkResults.flatMap { it.blockRows.map { blockRow -> blockRow.title } }
83 | }else {
84 | benchmarkResults.map { it.title }
85 | }
86 | println("titles: $titles -> ${benchmarkResults.map { it.blockRows }}")
87 | val wordColorMap = mutableMapOf()
88 | // TODO: Add more colors
89 | val lineColors = mutableListOf(
90 | "rgba(255, 99, 132, 1)",
91 | "rgba(54, 162, 235, 1)",
92 | "rgba(255, 206, 86, 1)",
93 | "rgba(75, 192, 192, 1)",
94 | "rgba(153, 102, 255, 1)",
95 | "rgba(255, 159, 64, 1)",
96 | )
97 | for (title in titles) {
98 | val firstWord = title.split(" ")[0]
99 | val color = wordColorMap.getOrPut(firstWord) {
100 |
101 | if (lineColors.isEmpty()) {
102 | lineColors.add("rgba(${randomRgb()}, ${randomRgb()}, ${randomRgb()}, 1)")
103 | }
104 |
105 | val newColor = lineColors.first()
106 | lineColors.remove(newColor)
107 | newColor
108 | }
109 | autoGroupMap[title] = color
110 | }
111 | return GroupMap(
112 | autoGroupMap = autoGroupMap,
113 | wordColorMap = wordColorMap
114 | ).also {
115 | println("groupMap: $it")
116 | }
117 | }
118 |
119 | private fun randomRgb() = (0..255).random()
120 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/core/TextNumberLine.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | private val digitRegex = "\\d+(.\\d+)?".toRegex()
4 |
5 | data class TextNumberLine(
6 | val text: String,
7 | val number: Float
8 | ) {
9 | companion object {
10 | private val AVGIZER_REGEX = "\\(input count : .+\\)\$".toRegex()
11 | fun parse(index : Int, iLine: String): TextNumberLine? {
12 | // Quick support for https://theapache64.github.io/avgizer/
13 | val match = AVGIZER_REGEX.find(iLine)
14 | val line = if (match != null){
15 | iLine.replace(match.groupValues.first(), "")
16 | } else {
17 | iLine
18 | }
19 |
20 | val number = digitRegex.findAll(line)
21 | .lastOrNull()
22 | ?.groupValues
23 | ?.firstOrNull()
24 | ?: return null
25 | val numberIndex = line.lastIndexOf(number)
26 | val newLine = line.substring(0, numberIndex)
27 | return TextNumberLine(newLine, number.toFloat())
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/commonMain/kotlin/model/Chart.kt:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import core.GroupMap
4 |
5 | data class ChartsBundle(
6 | val groupMap: GroupMap,
7 | val charts: List
8 | )
9 |
10 | data class Chart(
11 | val emoji: String,
12 | val label: String,
13 | // eg format: (before1 -> map { p50 -> 20, p90 -> 30 })
14 | val dataSets: Map>,
15 | val bsClass : String = "col-lg-6"
16 | )
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/model/FormData.kt:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | data class FormData(
4 | val data: String,
5 | val isTestNameDetectionEnabled : Boolean,
6 | val isAutoGroupEnabled : Boolean,
7 | val isLoading : Boolean,
8 | val loadingProgress : Int = 0
9 | )
10 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/model/SavedBenchmark.kt:
--------------------------------------------------------------------------------
1 | package model
2 |
3 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/Utils.kt:
--------------------------------------------------------------------------------
1 | inline fun jso(): T = js("({})")
2 |
3 | inline fun jso(builder: T.() -> Unit): T = jso().apply(builder)
--------------------------------------------------------------------------------
/src/jsMain/kotlin/chartjs/Type.kt:
--------------------------------------------------------------------------------
1 | package chartjs
2 |
3 | interface Type {
4 | companion object {
5 | inline val line: Type get() = Type("line")
6 | inline val bar: Type get() = Type("bar")
7 |
8 | inline val horizontalBar: Type get() = Type("horizontalBar")
9 | inline val radar: Type get() = Type("radar")
10 | inline val doughnut: Type get() = Type("doughnut")
11 | inline val polarArea: Type get() = Type("polarArea")
12 | inline val bubble: Type get() = Type("bubble")
13 | inline val pie: Type get() = Type("pie")
14 | inline val scatter: Type get() = Type("scatter")
15 | }
16 | }
17 |
18 | inline fun Type(value: String) = value.unsafeCast()
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/AutoFormUi.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.key
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import kotlinx.coroutines.delay
11 | import model.FormData
12 | import org.jetbrains.compose.web.attributes.ButtonType
13 | import org.jetbrains.compose.web.attributes.placeholder
14 | import org.jetbrains.compose.web.attributes.rows
15 | import org.jetbrains.compose.web.attributes.type
16 | import org.jetbrains.compose.web.css.marginRight
17 | import org.jetbrains.compose.web.css.marginTop
18 | import org.jetbrains.compose.web.css.percent
19 | import org.jetbrains.compose.web.css.px
20 | import org.jetbrains.compose.web.css.width
21 | import org.jetbrains.compose.web.dom.Button
22 | import org.jetbrains.compose.web.dom.Div
23 | import org.jetbrains.compose.web.dom.Form
24 | import org.jetbrains.compose.web.dom.H3
25 | import org.jetbrains.compose.web.dom.Label
26 | import org.jetbrains.compose.web.dom.Text
27 | import org.jetbrains.compose.web.dom.TextArea
28 |
29 | private val ALL_LOADING_MESSAGES = listOf(
30 | "Loading...",
31 | "Loading magic... This won't take long!",
32 | "Almost there! Great things are worth the wait.",
33 | "We're putting on the final touches. Stay with us!",
34 | "Looks like your network is slow 🤔... Hang tight!",
35 | "If this takes too long, try spinning in your chair!",
36 | "This is taking longer than usual. In the meantime, do 3 push-ups. Remember, health is wealth!",
37 | "Patience level: Jedi Master... Almost there!",
38 | )
39 |
40 |
41 | @Composable
42 | fun FormUi(
43 | form: FormData,
44 | shouldSelectUnsaved: Boolean,
45 | savedBenchmarks: List,
46 | onFormChanged: (form: FormData) -> Unit,
47 | onSaveClicked: (form: FormData) -> Unit,
48 | onShareClicked: (form: FormData) -> Unit,
49 | onSavedBenchmarkChanged: (key: String) -> Unit,
50 | onLoadBenchmarkClicked: (SavedBenchmarkNode) -> Unit,
51 | onDeleteBenchmarkClicked: (SavedBenchmarkNode) -> Unit,
52 | ) {
53 |
54 |
55 | LaunchedEffect(Unit) {
56 | onFormChanged(form)
57 | }
58 |
59 | H3 {
60 | Text("⌨️ Input")
61 | }
62 |
63 | Div {
64 | Form {
65 |
66 | key("inputForm") {
67 |
68 | SavedBenchmarksDropDown(
69 | shouldSelectUnsaved = shouldSelectUnsaved,
70 | savedBenchmarks = savedBenchmarks,
71 | onSavedBenchmarkChanged = onSavedBenchmarkChanged,
72 | onLoadBenchmarkClicked = onLoadBenchmarkClicked,
73 | onDeleteBenchmarkClicked = onDeleteBenchmarkClicked
74 | )
75 |
76 | Div(
77 | attrs = {
78 | classes("form-group")
79 | }
80 | ) {
81 |
82 | Label(
83 | forId = "benchmark",
84 | attrs = {
85 | classes("form-label")
86 | }
87 | ) {
88 | Text("Benchmark :")
89 | }
90 |
91 | TextArea(
92 | value = form.data
93 | ) {
94 | id("benchmark")
95 | classes("form-control")
96 | placeholder(value = "Benchmark data")
97 | rows(20)
98 | onInput { textInput ->
99 | onFormChanged(form.copy(data = textInput.value))
100 | }
101 | }
102 | }
103 |
104 | if (form.isLoading) {
105 | var progress by remember { mutableStateOf(20) }
106 | LaunchedEffect(Unit) {
107 | while (progress < 90) {
108 | delay(200)
109 | progress += 4
110 | }
111 | }
112 |
113 | var loadingMsg by remember { mutableStateOf("") }
114 | LaunchedEffect(Unit) {
115 | val loadingMessages = ALL_LOADING_MESSAGES.asReversed()
116 | .toMutableList()
117 | while (loadingMessages.isNotEmpty()) {
118 | loadingMsg = loadingMessages.removeAt(loadingMessages.lastIndex)
119 | delay(5000)
120 | }
121 | }
122 |
123 | Div(
124 | attrs = {
125 | classes("progress")
126 | style {
127 | marginTop(10.px)
128 | }
129 | }
130 | ) {
131 | Div(
132 | attrs = {
133 | classes("progress-bar", "progress-bar-striped", "progress-bar-animated", "bg-success")
134 | attr("role", "progressbar")
135 | attr("aria-valuenow", "$progress")
136 | attr("aria-valuemin", "0")
137 | attr("aria-valuemax", "100")
138 | style {
139 | width(progress.percent)
140 | }
141 | }
142 | ) {
143 | Text(loadingMsg)
144 | }
145 | }
146 | }
147 |
148 | Button(
149 | attrs = {
150 | classes("btn", "btn-dark", "float-end")
151 | style {
152 | marginTop(10.px)
153 | }
154 | if (form.data.isBlank()) {
155 | attr("disabled", "true")
156 | }
157 | onClick {
158 | onSaveClicked(form)
159 | }
160 | type(ButtonType.Button)
161 | }
162 | ) {
163 | Text("💾 SAVE")
164 | }
165 |
166 | Button(
167 | attrs = {
168 | classes("btn", "btn-dark", "float-end")
169 | style {
170 | marginTop(10.px)
171 | marginRight(10.px)
172 | }
173 | if (form.data.isBlank()) {
174 | attr("disabled", "true")
175 | }
176 | onClick {
177 | onShareClicked(form)
178 | }
179 | type(ButtonType.Button)
180 | }
181 | ) {
182 | Text("🔗 SHARE")
183 | }
184 | }
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/AutoGroupToggle.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.attributes.ButtonType
5 | import org.jetbrains.compose.web.attributes.type
6 | import org.jetbrains.compose.web.css.marginLeft
7 | import org.jetbrains.compose.web.css.px
8 | import org.jetbrains.compose.web.dom.*
9 |
10 | @Composable
11 | fun AutoGroup(
12 | isEnabled: Boolean,
13 | onButtonClicked: () -> Unit
14 | ) {
15 | Div(
16 | attrs = {
17 | classes("form-group")
18 | style {
19 | marginLeft(10.px)
20 | }
21 | }
22 | ) {
23 | // 🖌 Color map
24 |
25 | Label(
26 | forId = "colorMap",
27 | attrs = {
28 | classes("form-label")
29 | }
30 | ) {
31 | Text("Auto Group:")
32 | }
33 | Br()
34 | Button(
35 | attrs = {
36 | id("colorMap")
37 | classes("btn", if (isEnabled) "btn-success" else "btn-secondary")
38 | onClick {
39 | onButtonClicked()
40 | }
41 | type(ButtonType.Button)
42 | }
43 | ) {
44 | Text(if (isEnabled) "ON" else "OFF")
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/ChartUi.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import Chart
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.DisposableEffect
6 | import chartjs.Type
7 | import core.GroupMap
8 | import jso
9 | import org.jetbrains.compose.web.css.height
10 | import org.jetbrains.compose.web.css.maxHeight
11 | import org.jetbrains.compose.web.css.maxWidth
12 | import org.jetbrains.compose.web.css.percent
13 | import org.jetbrains.compose.web.css.px
14 | import org.jetbrains.compose.web.css.width
15 | import org.jetbrains.compose.web.dom.Canvas
16 | import org.jetbrains.compose.web.dom.H3
17 | import org.jetbrains.compose.web.dom.Text
18 |
19 | @Composable
20 | fun ChartUi(
21 | isColorMapEnabled: Boolean,
22 | groupMap: GroupMap,
23 | chartModel: model.Chart,
24 | onDotClicked : (focusGroup : String) -> Unit
25 | ) {
26 | H3 { Text("${chartModel.emoji} ${chartModel.label}") }
27 |
28 | // Charts
29 | Canvas(
30 | attrs = {
31 | style {
32 | width(100.percent)
33 | maxWidth(100.percent)
34 |
35 | height(700.px)
36 | maxHeight(700.px)
37 | }
38 | }
39 | ) {
40 | DisposableEffect(chartModel, isColorMapEnabled) {
41 | val dataSets = mutableListOf()
42 | for ((legend, values) in chartModel.dataSets) {
43 |
44 | dataSets.add(
45 | jso {
46 | label = legend
47 | data = values.values.toTypedArray()
48 | borderColor = if (isColorMapEnabled) {
49 | groupMap.autoGroupMap[label]
50 | } else {
51 | arrayOf(
52 | "rgba(54, 162, 235, 1)", // Blue
53 | "rgba(255, 159, 64, 1)", // Orange
54 | "rgba(75, 192, 192, 1)", // Teal
55 | "rgba(153, 102, 255, 1)", // Purple
56 | "rgba(255, 205, 86, 1)", // Yellow
57 | "rgba(22, 160, 133, 1)", // Green
58 | "rgba(142, 68, 173, 1)", // Deep Purple
59 | "rgba(230, 126, 34, 1)", // Burnt Orange
60 | "rgba(52, 152, 219, 1)", // Light Blue
61 | "rgba(46, 204, 113, 1)", // Emerald
62 | "rgba(155, 89, 182, 1)", // Amethyst
63 | "rgba(241, 196, 15, 1)", // Sun Yellow
64 | "rgba(127, 140, 141, 1)", // Asphalt
65 | "rgba(26, 188, 156, 1)", // Turquoise
66 | "rgba(201, 203, 207, 1)", // Grey
67 | "rgba(211, 84, 0, 1)", // Pumpkin
68 | "rgba(41, 128, 185, 1)", // Ocean Blue
69 | "rgba(39, 174, 96, 1)" // Nephritis
70 | )
71 | }
72 | borderWidth = 3
73 | }
74 | )
75 | }
76 | val chart = Chart(scopeElement, jso {
77 | type = Type.line
78 | val chartLabels = chartModel.dataSets.values.flatMap { it.keys }.toSet().toTypedArray()
79 | this.data = jso {
80 | labels = chartLabels
81 | datasets = dataSets.toTypedArray()
82 |
83 | }
84 | this.options = jso {
85 | plugins = jso {
86 | title = jso {
87 | display = true
88 | }
89 | }
90 | scales = jso {
91 | y = jso {
92 | beginAtZero = true
93 | }
94 | }
95 | onClick = { event: dynamic, elements: Array ->
96 | if (elements.isNotEmpty()) {
97 | val element = elements[0]
98 | val datasetIndex = element.datasetIndex
99 | val index = element.index
100 | val focusGroup = chartLabels[index as Int]
101 | onDotClicked(focusGroup)
102 | }
103 | }
104 | }
105 |
106 |
107 | })
108 | onDispose {
109 | chart.destroy()
110 | }
111 | }
112 | }
113 |
114 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/EditableTitle.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.attributes.InputType
5 | import org.jetbrains.compose.web.attributes.placeholder
6 | import org.jetbrains.compose.web.css.fontSize
7 | import org.jetbrains.compose.web.css.px
8 | import org.jetbrains.compose.web.dom.Div
9 | import org.jetbrains.compose.web.dom.Input
10 | import org.jetbrains.compose.web.dom.Label
11 | import org.jetbrains.compose.web.dom.Text
12 |
13 | @Composable
14 | fun EditableTitle() {
15 | Div(
16 | attrs = {
17 | classes("row")
18 | }
19 | ) {
20 | Div(
21 | attrs = {
22 | classes("form-group")
23 | }
24 | ) {
25 | Label(
26 | forId = "customTitle",
27 | attrs = {
28 | classes("form-label")
29 | }
30 | ) {
31 | Text("Title :")
32 | }
33 | Input(
34 | type = InputType.Text,
35 | ) {
36 | id("customTitle")
37 | classes("form-control")
38 | placeholder(value = "Custom title goes here")
39 | style {
40 | fontSize(24.px)
41 | }
42 | }
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/Error.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.dom.Div
5 | import org.jetbrains.compose.web.dom.H4
6 | import org.jetbrains.compose.web.dom.Text
7 |
8 | @Composable
9 | fun ErrorUi(message: String) {
10 | Div(attrs = {
11 | classes("row")
12 | }) {
13 | Div(attrs = {
14 | classes("col-lg-12")
15 | }) {
16 | H4(attrs = {
17 | classes("text-center")
18 | }) {
19 | Text("❌ $message")
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/FocusGroups.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.attributes.selected
5 | import org.jetbrains.compose.web.dom.*
6 |
7 |
8 | @Composable
9 | fun FocusGroups(
10 | focusGroups: List,
11 | currentFocusGroup: String?,
12 | onFocusGroupSelected: (focusGroup: String) -> Unit
13 | ){
14 | if(focusGroups.isNotEmpty()){
15 | Div(
16 | attrs = {
17 | classes("form-group")
18 | }
19 | ) {
20 | Label(
21 | forId = "focusGroups",
22 | attrs = {
23 | classes("form-label")
24 | }
25 | ) {
26 | Text("Focus Group :")
27 | }
28 | Select(
29 | attrs = {
30 | classes("form-select")
31 | id("focusGroups")
32 | onInput {
33 | it.value?.let { focusGroup ->
34 | onFocusGroupSelected(focusGroup)
35 | }
36 | }
37 | }
38 | ) {
39 | for (focusGroup in focusGroups) {
40 | Option(
41 | value = focusGroup,
42 | attrs = {
43 | if (focusGroup == currentFocusGroup) {
44 | selected()
45 | }
46 | }
47 | ) {
48 | Text(focusGroup)
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/Heading.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.css.marginBottom
5 | import org.jetbrains.compose.web.css.marginRight
6 | import org.jetbrains.compose.web.css.marginTop
7 | import org.jetbrains.compose.web.css.px
8 | import org.jetbrains.compose.web.css.width
9 | import org.jetbrains.compose.web.dom.Div
10 | import org.jetbrains.compose.web.dom.H1
11 | import org.jetbrains.compose.web.dom.Img
12 | import org.jetbrains.compose.web.dom.Text
13 |
14 |
15 | private const val version = "v25.03.16.0 (16 Mar 2025)"
16 |
17 | @Composable
18 | fun Heading() {
19 | Div(attrs = {
20 | classes("row")
21 | }) {
22 | Div(attrs = {
23 | classes("col-lg-12", "text-center")
24 | style {
25 | marginBottom(30.px)
26 | marginTop(30.px)
27 | }
28 |
29 | }) {
30 | H1(
31 | attrs = {
32 | attr("data-bs-toggle", "tooltip")
33 | attr("data-bs-placement", "top")
34 | attr("title", version)
35 | }
36 | ) {
37 | Img(
38 | src = "icons/apple-touch-icon.png",
39 | attrs = {
40 | style {
41 | width(36.px)
42 | marginRight(6.px)
43 | marginTop((-8).px)
44 | }
45 | }
46 | )
47 | Text("BenChart")
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/SavedBenchmarkNode.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class SavedBenchmarkNode(
7 | val key : String,
8 | val value : String
9 | )
10 |
11 | @Serializable
12 | data class SavedBenchmarks(
13 | var items : List
14 | )
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/SavedBenchmarksDropDown.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.*
4 | import org.jetbrains.compose.web.attributes.ButtonType
5 | import org.jetbrains.compose.web.attributes.disabled
6 | import org.jetbrains.compose.web.attributes.selected
7 | import org.jetbrains.compose.web.attributes.type
8 | import org.jetbrains.compose.web.css.marginRight
9 | import org.jetbrains.compose.web.css.px
10 | import org.jetbrains.compose.web.dom.*
11 | import kotlin.js.Date
12 |
13 | val KEY_UNSAVED_BENCHMARK = "unsavedBenchmark_${Date().getMilliseconds()}"
14 |
15 | @Composable
16 | fun SavedBenchmarksDropDown(
17 | shouldSelectUnsaved: Boolean,
18 | savedBenchmarks: List,
19 | onSavedBenchmarkChanged: (key: String) -> Unit,
20 | onLoadBenchmarkClicked: (SavedBenchmarkNode) -> Unit,
21 | onDeleteBenchmarkClicked: (SavedBenchmarkNode) -> Unit
22 | ) {
23 |
24 | if (savedBenchmarks.isEmpty()) {
25 | return
26 | }
27 |
28 | var selectedBenchmark by remember(savedBenchmarks) { mutableStateOf(savedBenchmarks.first()) }
29 |
30 |
31 | Label(
32 | forId = "savedBenchmarks",
33 | attrs = {
34 | classes("form-label")
35 | }
36 | ) {
37 | Text("Load Benchmark :")
38 | }
39 |
40 | Div(
41 | attrs = {
42 | classes("form-group")
43 | }
44 | ) {
45 | Div(
46 | attrs = {
47 | classes("row")
48 | }
49 | ) {
50 |
51 | Div(
52 | attrs = {
53 | classes("col")
54 | }
55 | ) {
56 | Select(
57 | attrs = {
58 | classes("form-select")
59 | id("savedBenchmarks")
60 | onChange {
61 | it.value?.let { benchmarkKey ->
62 | onSavedBenchmarkChanged(benchmarkKey)
63 | selectedBenchmark =
64 | savedBenchmarks.find { benchmark -> benchmark.key == benchmarkKey }!!
65 | }
66 | }
67 | }
68 | ) {
69 | for (savedBenchmark in savedBenchmarks) {
70 | Option(
71 | value = savedBenchmark.key,
72 | attrs = {
73 | if (savedBenchmark.key == selectedBenchmark.key && !shouldSelectUnsaved) {
74 | selected()
75 | }
76 | }
77 | ) {
78 | Text(savedBenchmark.key)
79 | }
80 | }
81 |
82 | Option(
83 | value = KEY_UNSAVED_BENCHMARK,
84 | attrs = {
85 | if (shouldSelectUnsaved) {
86 | selected()
87 | }
88 | }
89 | ) {
90 | Text("Unsaved benchmark")
91 | }
92 | }
93 | }
94 |
95 | Div(
96 | attrs = {
97 | classes("col")
98 | }
99 | ) {
100 | Button(
101 | attrs = {
102 | classes("btn", "btn-primary")
103 | style {
104 | marginRight(10.px)
105 | }
106 | onClick {
107 | onLoadBenchmarkClicked(selectedBenchmark)
108 | }
109 | type(ButtonType.Button)
110 |
111 | if (shouldSelectUnsaved) {
112 | disabled()
113 | }
114 | }
115 | ) {
116 | Text("LOAD")
117 | }
118 |
119 | Button(
120 | attrs = {
121 | classes("btn", "btn-danger")
122 | onClick {
123 | onDeleteBenchmarkClicked(selectedBenchmark)
124 | }
125 | type(ButtonType.Button)
126 |
127 | if (shouldSelectUnsaved) {
128 | disabled()
129 | }
130 | }
131 | ) {
132 | Text("DELETE")
133 | }
134 | }
135 | }
136 | }
137 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/StandardDeviationUi.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.key
5 | import org.jetbrains.compose.web.attributes.href
6 | import org.jetbrains.compose.web.css.CSSColorValue
7 | import org.jetbrains.compose.web.css.Color
8 | import org.jetbrains.compose.web.css.color
9 | import org.jetbrains.compose.web.css.textAlign
10 | import org.jetbrains.compose.web.dom.A
11 | import org.jetbrains.compose.web.dom.Table
12 | import org.jetbrains.compose.web.dom.Tbody
13 | import org.jetbrains.compose.web.dom.Td
14 | import org.jetbrains.compose.web.dom.Text
15 | import org.jetbrains.compose.web.dom.Th
16 | import org.jetbrains.compose.web.dom.Thead
17 | import org.jetbrains.compose.web.dom.Tr
18 |
19 | data class SDNode(
20 | val name: String,
21 | val population: List,
22 | val standardDeviation: Float,
23 | val errorMargin: Map,
24 | val min : Float,
25 | val median :Float,
26 | val max: Float,
27 | val percentiles : Map
28 | )
29 |
30 |
31 | @Composable
32 | fun StandardDeviationUi(
33 | groupName: String,
34 | sdNodes: List
35 | ) {
36 | Table(
37 | attrs = {
38 | attr("border", "1")
39 | classes("table", "table-bordered")
40 | }
41 | ) {
42 | Thead {
43 | Tr {
44 | Th(
45 | attrs = {
46 | attr("rowspan", "2")
47 | }
48 | ) {
49 | Text(groupName)
50 | }
51 | Th(
52 | attrs = {
53 | attr("rowspan", "2")
54 | }
55 | ) {
56 | Text("Std. Deviation")
57 | }
58 | Th(
59 | attrs = {
60 | attr("colspan", "${sdNodes.firstOrNull()?.errorMargin?.size ?: 0}")
61 | style {
62 | textAlign("center")
63 | }
64 | }
65 | ) {
66 | Text("Error Margin")
67 | }
68 | }
69 | Tr {
70 | sdNodes.firstOrNull()?.errorMargin?.keys?.forEach { emKey ->
71 | key(emKey) {
72 | Th { Text(emKey) }
73 | }
74 | }
75 | }
76 | }
77 | Tbody {
78 | for (sdNode in sdNodes) {
79 | key(sdNode.toString()) {
80 | Tr {
81 | Td { Text(sdNode.name) }
82 | Td(
83 | attrs = {
84 | title("${sdNode.population}")
85 | }
86 | ) {
87 | A(
88 | attrs = {
89 | href(
90 | "https://www.calculator.net/standard-deviation-calculator.html?numberinputs=${
91 | sdNode.population.joinToString(
92 | separator = ","
93 | )
94 | }&ctype=p&x=Calculate"
95 | )
96 | style {
97 | color(Color.black)
98 | }
99 | }
100 | ) {
101 | Text(sdNode.standardDeviation.toString())
102 | }
103 | }
104 |
105 | sdNode.errorMargin.values.forEach { margin ->
106 | Td { Text("$margin%") }
107 | }
108 | }
109 | }
110 | }
111 | }
112 | }
113 | }
114 |
115 | @Composable
116 | fun Stats(
117 | groupName: String,
118 | sdNodes: List
119 | ) {
120 | Table(
121 | attrs = {
122 | attr("border", "1")
123 | classes("table", "table-bordered")
124 | }
125 | ) {
126 | Thead {
127 | Tr {
128 | Th(
129 | attrs = {
130 | attr("rowspan", "2")
131 | }
132 | ) {
133 | Text(groupName)
134 | }
135 | Th(
136 | attrs = {
137 | attr("rowspan", "2")
138 | }
139 | ) {
140 | Text("Min")
141 | }
142 | Th(
143 | attrs = {
144 | attr("rowspan", "2")
145 | }
146 | ) {
147 | Text("Median")
148 | }
149 |
150 | Th(
151 | attrs = {
152 | attr("rowspan", "2")
153 | }
154 | ) {
155 | Text("Max")
156 | }
157 | Th(
158 | attrs = {
159 | attr("colspan", "${sdNodes.firstOrNull()?.percentiles?.size ?: 0}")
160 | style {
161 | textAlign("center")
162 | }
163 | }
164 | ) {
165 | Text("Percentiles")
166 | }
167 | }
168 | Tr {
169 | sdNodes.firstOrNull()?.percentiles?.keys?.forEach { emKey ->
170 | key(emKey) {
171 | Th { Text(emKey) }
172 | }
173 | }
174 | }
175 | }
176 | Tbody {
177 | for (sdNode in sdNodes) {
178 | key(sdNode.toString()) {
179 | Tr {
180 | Td { Text(sdNode.name) }
181 | Td(
182 | attrs = {
183 | title("${sdNode.population.sorted()}")
184 | }
185 | ) {
186 | Text(sdNode.min.toString())
187 | }
188 |
189 | Td(
190 | attrs = {
191 | title("${sdNode.population}")
192 | }
193 | ) {
194 | Text(sdNode.median.toString())
195 | }
196 |
197 | Td(
198 | attrs = {
199 | title("${sdNode.population.sortedDescending()}")
200 | }
201 | ) {
202 | Text(sdNode.max.toString())
203 | }
204 |
205 |
206 | sdNode.percentiles.values.forEach { percentile ->
207 | Td { Text("$percentile") }
208 | }
209 | }
210 | }
211 | }
212 | }
213 | }
214 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/Summary.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.key
5 | import core.BenchmarkResult.Companion.FOCUS_GROUP_ALL
6 | import core.MetricUnit
7 | import kotlinx.browser.document
8 | import org.jetbrains.compose.web.attributes.AttrsScope
9 | import org.jetbrains.compose.web.attributes.ButtonType
10 | import org.jetbrains.compose.web.attributes.selected
11 | import org.jetbrains.compose.web.attributes.type
12 | import org.jetbrains.compose.web.css.fontSize
13 | import org.jetbrains.compose.web.css.fontWeight
14 | import org.jetbrains.compose.web.css.px
15 | import org.jetbrains.compose.web.dom.AttrBuilderContext
16 | import org.jetbrains.compose.web.dom.Br
17 | import org.jetbrains.compose.web.dom.Button
18 | import org.jetbrains.compose.web.dom.ContentBuilder
19 | import org.jetbrains.compose.web.dom.Div
20 | import org.jetbrains.compose.web.dom.ElementBuilder
21 | import org.jetbrains.compose.web.dom.H3
22 | import org.jetbrains.compose.web.dom.Li
23 | import org.jetbrains.compose.web.dom.Option
24 | import org.jetbrains.compose.web.dom.P
25 | import org.jetbrains.compose.web.dom.Select
26 | import org.jetbrains.compose.web.dom.Small
27 | import org.jetbrains.compose.web.dom.Span
28 | import org.jetbrains.compose.web.dom.TagElement
29 | import org.jetbrains.compose.web.dom.Text
30 | import org.jetbrains.compose.web.dom.Ul
31 | import org.w3c.dom.Element
32 | import org.w3c.dom.HTMLElement
33 | import org.w3c.dom.HTMLSpanElement
34 | import kotlin.math.absoluteValue
35 |
36 | // P50 : After performed 25% better (-30ms)
37 | class SummaryNode(
38 | val isGeneric: Boolean,
39 | val emoji: String,
40 | val segment: String,
41 | val label: String,
42 | val percentage: Float,
43 | val stateWord: String,
44 | val diff: Float,
45 | val diffSymbol: String,
46 | val after: Float,
47 | val before: Float,
48 | val badgeClass: String,
49 | val unit: MetricUnit?,
50 | )
51 |
52 | data class Summary(
53 | val title: String,
54 | val nodes: List
55 | )
56 |
57 | @Composable
58 | fun SummaryContainer(
59 | selector: @Composable () -> Unit,
60 | oldSummaries: List,
61 | newSummaries: List,
62 | oldAvgOfCount: Int,
63 | newAvgOfCount: Int,
64 | currentFocusedGroup: String
65 | ) {
66 |
67 | selector()
68 | for ((index, summaries) in listOf(oldSummaries to oldAvgOfCount, newSummaries to newAvgOfCount).withIndex()) {
69 | key("summaries-$index") {
70 | if (summaries.first.isNotEmpty()) {
71 | Br()
72 |
73 | for (summary in summaries.first) {
74 | key(summary.title + index) {
75 | SummaryUi(summary.title, summaries.second, summary.nodes, currentFocusedGroup)
76 | Br()
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
84 | @Composable
85 | fun SummarySelector(
86 | bestButtonLabel: String,
87 | worstButtonLabel: String,
88 | onBestClicked: () -> Unit,
89 | onWorstClicked: () -> Unit,
90 | blockNames: List,
91 | selectedBlockNameOne: String?,
92 | selectedBlockNameTwo: String?,
93 | onBlockOneSelected: (String) -> Unit,
94 | onBlockTwoSelected: (String) -> Unit,
95 | ) {
96 |
97 | Div(
98 | attrs = {
99 | classes("row", "mb-3")
100 | }
101 | ) {
102 |
103 | Div(
104 | attrs = {
105 | classes("col-auto")
106 | }
107 | ) {
108 | // Best
109 | Button(
110 | attrs = {
111 | classes("btn", "btn-outline-dark", "btn-sm")
112 | onClick {
113 | onBestClicked()
114 | }
115 | type(ButtonType.Button)
116 | }
117 | ) {
118 | Text(bestButtonLabel)
119 | }
120 |
121 | }
122 | Div(
123 | attrs = {
124 | classes("col-auto")
125 | }
126 | ) {
127 | // Best
128 | Button(
129 | attrs = {
130 | classes("btn", "btn-outline-dark", "btn-sm")
131 | onClick {
132 | onWorstClicked()
133 | }
134 | type(ButtonType.Button)
135 | }
136 | ) {
137 | Text(worstButtonLabel)
138 | }
139 | }
140 |
141 | }
142 |
143 | Div(
144 | attrs = {
145 | classes("row")
146 | }
147 | ) {
148 | repeat(2) { index ->
149 | key("block-selector-$index") {
150 | Div(
151 | attrs = {
152 | classes("col")
153 | }
154 | ) {
155 | Select(
156 | attrs = {
157 | classes("form-select")
158 | onInput {
159 | it.value?.let { newBlockName ->
160 | if (index == 0) {
161 | // first block name
162 | onBlockOneSelected(newBlockName)
163 | } else {
164 | // second block name
165 | onBlockTwoSelected(newBlockName)
166 | }
167 | }
168 | }
169 | }
170 | ) {
171 | for (blockName in blockNames) {
172 | Option(
173 | value = blockName,
174 | attrs = {
175 | val selectedBlockName =
176 | if (index == 0) selectedBlockNameOne else selectedBlockNameTwo
177 | if (blockName == selectedBlockName) {
178 | selected()
179 | }
180 | }
181 | ) {
182 | Text(blockName)
183 | }
184 | }
185 | }
186 | }
187 |
188 | if (index == 0) {
189 | Div(
190 | attrs = {
191 | classes("col-auto")
192 | }
193 | ) {
194 | P {
195 | Strong {
196 | Text("vs")
197 | }
198 | }
199 | }
200 |
201 | }
202 | }
203 | }
204 |
205 | }
206 |
207 |
208 | }
209 |
210 | private open class ElementBuilderImplementation(private val tagName: String) :
211 | ElementBuilder {
212 | private val el: Element by lazy { document.createElement(tagName) }
213 |
214 | @Suppress("UNCHECKED_CAST")
215 | override fun create(): TElement = el.cloneNode() as TElement
216 | }
217 |
218 | private val Strong: ElementBuilder = ElementBuilderImplementation("strong")
219 |
220 | @Composable
221 | fun Strong(
222 | attrs: AttrBuilderContext? = null,
223 | content: ContentBuilder? = null
224 | ) = TagElement(elementBuilder = Strong, applyAttrs = attrs, content = content)
225 |
226 | @Composable
227 | fun SummaryUi(title: String, avgOfCount: Int, summary: List, currentFocusGroup: String) {
228 | Div(
229 | attrs = {
230 | classes("row")
231 | }
232 | ) {
233 | H3 {
234 | Text(title)
235 | if (avgOfCount >= 1) {
236 | Small(
237 | attrs = {
238 | classes("text-muted")
239 | style {
240 | fontSize(18.px)
241 | }
242 | }
243 | ) {
244 | if (avgOfCount == 1) {
245 | if (currentFocusGroup != FOCUS_GROUP_ALL) {
246 | Text(" (focused on '$currentFocusGroup')")
247 | }
248 | } else {
249 | Text(" (average of $avgOfCount)")
250 | }
251 | }
252 | }
253 | }
254 | Ul {
255 | summary.forEach { node ->
256 | Li {
257 | Text("${node.emoji} ")
258 | BoldText(
259 | text = node.segment,
260 | style = {
261 | classes("text-capitalize")
262 | }
263 | )
264 | Text(" : ")
265 | BoldText(node.label)
266 | Text(if (node.isGeneric) " looks " else " performed ")
267 | if (node.diff != 0f) {
268 | BoldText("${node.percentage}% ")
269 | }
270 | val postfix = node.getPostfix(node.diff)
271 | val beforePostfix = node.getPostfix(node.before)
272 | val afterPostfix = node.getPostfix(node.after)
273 |
274 | Span(
275 | attrs = {
276 | classes("badge", "bg-${node.badgeClass}")
277 |
278 | attr("data-bs-toggle", "tooltip")
279 | attr("data-bs-placement", "top")
280 |
281 | attr("title", if(node.diff ==0f) "both ${node.before}$beforePostfix" else "${node.before}$beforePostfix to ${node.after}$afterPostfix")
282 | }
283 | ) {
284 | Text(node.stateWord)
285 | }
286 | Text(" (${node.diffSymbol}${node.diff}$postfix)")
287 | }
288 | }
289 | }
290 | }
291 | }
292 |
293 | fun SummaryNode.getPostfix(num: Float): String {
294 | return unit?.let { unit ->
295 | if (num.absoluteValue > 1) unit.plural else unit.singular
296 | } ?: ""
297 | }
298 | fun String.predictTitle(): String {
299 | var name = this
300 | if (name.endsWith("Mah", ignoreCase = false)) {
301 | name = name.replace("Mah", "")
302 | }
303 | if (name.endsWith("Ms", ignoreCase = false)) {
304 | name = name.replace("Ms", "")
305 | }
306 | return name.map {
307 | if (it.isUpperCase()) {
308 | " $it"
309 | } else {
310 | it.toString()
311 | }
312 | }.joinToString(separator = "").replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
313 | }
314 |
315 | @Composable
316 | private fun BoldText(
317 | text: String,
318 | style: (AttrsScope.() -> Unit)? = null
319 | ) {
320 | Span(
321 | attrs = {
322 | style?.invoke(this)
323 | style {
324 | fontWeight("bold")
325 | }
326 | }
327 | ) {
328 | Text(text)
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/TestNameDetectionToggle.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.attributes.ButtonType
5 | import org.jetbrains.compose.web.attributes.type
6 | import org.jetbrains.compose.web.css.marginLeft
7 | import org.jetbrains.compose.web.css.px
8 | import org.jetbrains.compose.web.dom.*
9 |
10 | @Composable
11 | fun TestNameDetectionToggle(
12 | isEnabled: Boolean,
13 | onButtonClicked: () -> Unit
14 | ) {
15 | Div(
16 | attrs = {
17 | classes("form-group")
18 | style {
19 | marginLeft(10.px)
20 | }
21 | }
22 | ) {
23 | // 🖌 Color map
24 |
25 | Label(
26 | forId = "testNameDetection",
27 | attrs = {
28 | classes("form-label")
29 | }
30 | ) {
31 | Text("Test Name Detection:")
32 | }
33 | Br()
34 | Button(
35 | attrs = {
36 | id("testNameDetection")
37 | classes("btn", if (isEnabled) "btn-success" else "btn-secondary")
38 | onClick {
39 | onButtonClicked()
40 | }
41 | type(ButtonType.Button)
42 | }
43 | ) {
44 | Text(if (isEnabled) "ON" else "OFF")
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/components/TestNames.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.attributes.selected
5 | import org.jetbrains.compose.web.dom.*
6 |
7 |
8 | @Composable
9 | fun TestNames(
10 | testNames: List,
11 | currentTestName: String? = null,
12 | onTestNameSelected: (option: String) -> Unit
13 | ){
14 | if(testNames.isNotEmpty()){
15 | Div(
16 | attrs = {
17 | classes("form-group")
18 | }
19 | ) {
20 | Label(
21 | forId = "testNames",
22 | attrs = {
23 | classes("form-label")
24 | }
25 | ) {
26 | Text("Test Name :")
27 | }
28 | Select(
29 | attrs = {
30 | classes("form-select")
31 | id("testNames")
32 | onInput {
33 | it.value?.let { newTestName ->
34 | onTestNameSelected(newTestName)
35 | }
36 | }
37 | }
38 | ) {
39 | for (testName in testNames) {
40 | Option(
41 | value = testName,
42 | attrs = {
43 | if (testName == currentTestName) {
44 | selected()
45 | }
46 | }
47 | ) {
48 | Text(testName)
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.web.renderComposable
2 | import page.home.HomePageUi
3 |
4 | const val IS_INJECT_DUMMY = true
5 |
6 | fun main() {
7 |
8 | initChartSettings()
9 | renderComposable(rootElementId = "root") {
10 | HomePageUi()
11 | }
12 | }
13 |
14 | private fun initChartSettings() {
15 | Chart.register(
16 | ArcElement,
17 | LineElement,
18 | BarElement,
19 | PointElement,
20 | BarController,
21 | BubbleController,
22 | DoughnutController,
23 | LineController,
24 | PieController,
25 | PolarAreaController,
26 | RadarController,
27 | ScatterController,
28 | CategoryScale,
29 | LinearScale,
30 | LogarithmicScale,
31 | RadialLinearScale,
32 | TimeScale,
33 | TimeSeriesScale,
34 | Decimation,
35 | Filler,
36 | Legend,
37 | Title,
38 | Tooltip,
39 | SubTitle
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/page/home/HomePage.kt:
--------------------------------------------------------------------------------
1 | package page.home
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import components.AutoGroup
6 | import components.ChartUi
7 | import components.EditableTitle
8 | import components.ErrorUi
9 | import components.FocusGroups
10 | import components.FormUi
11 | import components.Heading
12 | import components.StandardDeviationUi
13 | import components.Stats
14 | import components.SummaryContainer
15 | import components.SummarySelector
16 | import components.TestNameDetectionToggle
17 | import components.TestNames
18 | import core.InputType
19 | import org.jetbrains.compose.web.css.paddingBottom
20 | import org.jetbrains.compose.web.css.paddingLeft
21 | import org.jetbrains.compose.web.css.paddingRight
22 | import org.jetbrains.compose.web.css.px
23 | import org.jetbrains.compose.web.dom.Br
24 | import org.jetbrains.compose.web.dom.Div
25 | import org.jetbrains.compose.web.dom.Form
26 | import org.jetbrains.compose.web.dom.H3
27 | import org.jetbrains.compose.web.dom.Text
28 | import repo.BenchmarkRepoImpl
29 | import repo.FormRepoImpl
30 | import repo.GoogleFormRepoImpl
31 | import repo.GoogleSheetRepoImpl
32 | import repo.UserRepoImpl
33 |
34 | @Composable
35 | fun HomePageUi(
36 | viewModel: HomeViewModel = remember {
37 | HomeViewModel(
38 | BenchmarkRepoImpl(),
39 | FormRepoImpl(),
40 | GoogleFormRepoImpl(),
41 | GoogleSheetRepoImpl(),
42 | UserRepoImpl()
43 | )
44 | }
45 | ) {
46 | Div(
47 | attrs = {
48 | classes("container-fluid")
49 | }
50 | ) {
51 |
52 | // Heading
53 | Heading()
54 |
55 | // Error
56 | if (viewModel.errorMsg.isNotBlank()) {
57 | ErrorUi(viewModel.errorMsg)
58 | }
59 |
60 |
61 | // Main
62 | Div(attrs = {
63 | classes("row")
64 | style {
65 | paddingLeft(40.px)
66 | paddingRight(40.px)
67 | paddingBottom(40.px)
68 | }
69 | }) {
70 | Div(attrs = {
71 | classes("col-lg-4")
72 | }) {
73 | FormUi(
74 | form = viewModel.form,
75 | shouldSelectUnsaved = viewModel.shouldSelectUnsaved,
76 | onFormChanged = viewModel::onFormChanged,
77 | onSaveClicked = viewModel::onSaveClicked,
78 | savedBenchmarks = viewModel.savedBenchmarks,
79 | onSavedBenchmarkChanged = viewModel::onSavedBenchmarkChanged,
80 | onLoadBenchmarkClicked = viewModel::onLoadBenchmarkClicked,
81 | onDeleteBenchmarkClicked = viewModel::onDeleteBenchmarkClicked,
82 | onShareClicked = viewModel::onShareClicked
83 | )
84 |
85 | Br()
86 | Br()
87 |
88 | SummaryContainer(
89 | selector = {
90 | println("block size ${viewModel.blockNames.size}")
91 | if (viewModel.blockNames.size > 2) {
92 | SummarySelector(
93 | bestButtonLabel = "BEST (-${viewModel.bestAggSummary?.sumOfGreen}${viewModel.unit})",
94 | worstButtonLabel = "WORST (+${viewModel.worstAggSummary?.sumOfRed}${viewModel.unit})",
95 | onBestClicked = viewModel::onBestClicked,
96 | onWorstClicked = viewModel::onWorstClicked,
97 | blockNames = viewModel.blockNames,
98 | selectedBlockNameOne = viewModel.selectedBlockNameOne,
99 | selectedBlockNameTwo = viewModel.selectedBlockNameTwo,
100 | onBlockOneSelected = viewModel::onBlockNameOneChanged,
101 | onBlockTwoSelected = viewModel::onBlockNameTwoChanged
102 | )
103 | }
104 | },
105 | newSummaries = viewModel.summaries,
106 | oldSummaries = viewModel.oldSummaries,
107 | newAvgOfCount = viewModel.avgOfCount,
108 | oldAvgOfCount = viewModel.oldAvgOfCount,
109 | currentFocusedGroup = viewModel.currentFocusedGroup
110 | )
111 | }
112 |
113 | viewModel.chartsBundle?.charts?.takeIf { it.isNotEmpty() }?.let { fullChartsList ->
114 | val mainCharts = viewModel.chartsBundle ?: error("TSH")
115 | Div(
116 | attrs = {
117 | classes("col-lg-8")
118 | }
119 | ) {
120 |
121 | if (viewModel.isEditableTitleEnabled) {
122 | EditableTitle()
123 | } else {
124 | H3(
125 | attrs = {
126 | onDoubleClick {
127 | viewModel.onTitleDoubleClicked()
128 | }
129 | }
130 | ) {
131 | Text("🖥 Output")
132 | }
133 | }
134 |
135 | // 🧪 ToolBar
136 | Div(
137 | attrs = {
138 | classes("row")
139 | }
140 | ) {
141 | Form {
142 | Div(
143 | attrs = {
144 | classes("row")
145 | }
146 | ) {
147 |
148 | if (viewModel.isAutoGroupButtonVisible) {
149 | Div(
150 | attrs = {
151 | classes("col-md-2")
152 | }
153 | ) {
154 | AutoGroup(
155 | isEnabled = viewModel.form.isAutoGroupEnabled,
156 | onButtonClicked = viewModel::onToggleAutoGroupClicked
157 | )
158 | }
159 | }
160 |
161 | if (viewModel.focusGroups.size > 1) {
162 | Div(
163 | attrs = {
164 | classes("col-md-4")
165 | }
166 | ) {
167 | FocusGroups(
168 | focusGroups = viewModel.focusGroups,
169 | currentFocusGroup = viewModel.currentFocusedGroup,
170 | onFocusGroupSelected = { focusGroup ->
171 | viewModel.onFocusGroupSelected(focusGroup)
172 | }
173 | )
174 | }
175 | }
176 |
177 | if (viewModel.inputType == InputType.MACRO_BENCHMARK) {
178 | Div(
179 | attrs = {
180 | classes("col-md-2")
181 | }
182 | ) {
183 | TestNameDetectionToggle(
184 | isEnabled = viewModel.form.isTestNameDetectionEnabled,
185 | onButtonClicked = viewModel::onToggleTestNameDetectionClicked
186 | )
187 | }
188 | }
189 |
190 | if (viewModel.testNames.isNotEmpty()) {
191 | Div(
192 | attrs = {
193 | classes("col-md-4")
194 | }
195 | ) {
196 | TestNames(
197 | testNames = viewModel.testNames,
198 | onTestNameSelected = { newTestName ->
199 | viewModel.onTestNameSelected(newTestName)
200 | }
201 | )
202 | }
203 |
204 | }
205 |
206 | }
207 | }
208 | }
209 |
210 | Br()
211 | val chunkedCharts = remember(fullChartsList) { fullChartsList.chunked(2) }
212 |
213 |
214 | // 📊 Charts
215 | for (charts in chunkedCharts) {
216 | Div(
217 | attrs = {
218 | classes("row")
219 | }
220 | ) {
221 | for (chart in charts) {
222 | // 📊 duration chart
223 | Div(attrs = {
224 | classes(chart.bsClass)
225 | }) {
226 | ChartUi(
227 | isColorMapEnabled = viewModel.form.isAutoGroupEnabled,
228 | groupMap = mainCharts.groupMap,
229 | chartModel = chart,
230 | onDotClicked = viewModel::onDotClicked,
231 | )
232 | }
233 | }
234 | }
235 | }
236 |
237 |
238 | Br()
239 |
240 | // Summary
241 | if(viewModel.sdNodes.isNotEmpty()){
242 | Div(
243 | attrs = {
244 | classes("row")
245 | }
246 | ) {
247 | Div(
248 | attrs = {
249 | classes("col-md-6")
250 | }
251 | ) {
252 | H3 {
253 | Text("📈 Standard Deviation: ")
254 | }
255 |
256 | StandardDeviationUi(viewModel.currentFocusedGroup, viewModel.sdNodes)
257 | }
258 |
259 | Div(
260 | attrs = {
261 | classes("col-md-6")
262 | }
263 | ) {
264 | H3 {
265 | Text("📈 Statistical Summary: ")
266 | }
267 |
268 | Stats(viewModel.currentFocusedGroup, viewModel.sdNodes)
269 | }
270 | }
271 |
272 |
273 | }
274 | }
275 | }
276 |
277 |
278 | }
279 | }
280 |
281 | ShareAwareModal(
282 | onShareClicked = {
283 | viewModel.onAwarePublicShare()
284 | }
285 | )
286 |
287 | SharedModal(
288 | shareUrl = viewModel.sharedUrl,
289 | onCopyToClipboardClicked = { sharedUrl ->
290 | viewModel.onCopyToClipboardClicked(sharedUrl)
291 | }
292 | )
293 | }
294 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/page/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package page.home
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateListOf
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import components.KEY_UNSAVED_BENCHMARK
9 | import components.SDNode
10 | import components.SavedBenchmarkNode
11 | import components.Summary
12 | import core.BenchmarkResult
13 | import core.BenchmarkResult.Companion.FOCUS_GROUP_ALL
14 | import core.InputType
15 | import core.toCharts
16 | import core.toGenericChart
17 | import kotlinx.browser.window
18 | import model.ChartsBundle
19 | import model.FormData
20 | import org.w3c.dom.events.KeyboardEvent
21 | import repo.BenchmarkRepo
22 | import repo.FormRepo
23 | import repo.GoogleFormRepo
24 | import repo.GoogleSheetRepo
25 | import repo.UserRepo
26 | import utils.DefaultValues
27 | import utils.RandomString
28 | import utils.SummaryUtils
29 | import utils.calculateErrorMargins
30 | import kotlin.js.Date
31 | import kotlin.math.min
32 |
33 | external fun setTimeout(handler: dynamic, timeout: Int): Int
34 | external fun clearTimeout(timeoutId: Int)
35 |
36 | data class ConfidenceIntervals(
37 | val mean: Float,
38 | // Absolute margins
39 | val marginOf68p3: Float,
40 | val marginOf90: Float,
41 | val marginOf95: Float,
42 | val marginOf99: Float,
43 | // Percentage margins
44 | val percentageMarginOf68p3: Float,
45 | val percentageMarginOf90: Float,
46 | val percentageMarginOf95: Float,
47 | val percentageMarginOf99: Float,
48 | val sampleSize: Int,
49 | val standardDeviation: Float
50 | )
51 |
52 |
53 | @Stable
54 | class HomeViewModel(
55 | private val benchmarkRepo: BenchmarkRepo,
56 | private val formRepo: FormRepo,
57 | private val googleFormRepo: GoogleFormRepo,
58 | private val googleSheetRepo: GoogleSheetRepo,
59 | private val userRepo: UserRepo
60 | ) {
61 |
62 | companion object {
63 | private const val ERROR_GENERIC = "Something went wrong!"
64 |
65 | // keys
66 | const val RETRY_COUNT = 3
67 | }
68 |
69 |
70 | var savedBenchmarks by mutableStateOf>(emptyList())
71 | private set
72 |
73 | // States
74 | private var currentTestName: String? = null
75 |
76 | var testNames = mutableStateListOf()
77 | private set
78 |
79 |
80 | var currentFocusedGroup by mutableStateOf(FOCUS_GROUP_ALL)
81 | private set
82 |
83 | var focusGroups = mutableStateListOf()
84 | private set
85 |
86 | var chartsBundle by mutableStateOf(null)
87 | private set
88 |
89 | var errorMsg by mutableStateOf("")
90 | private set
91 |
92 | var isEditableTitleEnabled by mutableStateOf(false)
93 | private set
94 |
95 | var shouldSelectUnsaved by mutableStateOf(false)
96 | private set
97 |
98 | var selectedBlockNameOne by mutableStateOf(null)
99 | private set
100 |
101 | var selectedBlockNameTwo by mutableStateOf(null)
102 | private set
103 |
104 | var blockNames = mutableStateListOf()
105 | private set
106 |
107 | var sdNodes = mutableStateListOf()
108 | private set
109 |
110 | var oldAvgOfCount by mutableStateOf(-1)
111 | private set
112 |
113 | var avgOfCount by mutableStateOf(-1)
114 | private set
115 |
116 | var isAutoGroupButtonVisible by mutableStateOf(false)
117 | private set
118 |
119 | var oldSummaries = mutableStateListOf()
120 | private set
121 |
122 | var summaries = mutableStateListOf()
123 | private set
124 |
125 | var inputType by mutableStateOf(null)
126 | private set
127 |
128 | var unit by mutableStateOf("")
129 | private set
130 |
131 | var bestAggSummary by mutableStateOf(null)
132 | private set
133 |
134 | var worstAggSummary by mutableStateOf(null)
135 | private set
136 |
137 | var sharedUrl by mutableStateOf(null)
138 | private set
139 |
140 | var form by mutableStateOf(
141 | FormData(
142 | data = "",
143 | isTestNameDetectionEnabled = false,
144 | isAutoGroupEnabled = false,
145 | isLoading = true
146 | )
147 | )
148 | private set
149 |
150 | init {
151 | refreshBenchmarks()
152 |
153 | // set key press listener on window
154 | window.addEventListener("keydown", {
155 | val event = it.unsafeCast()
156 | if (event.key == "Escape") {
157 | onFocusGroupSelected(FOCUS_GROUP_ALL)
158 | }
159 | })
160 |
161 | // Reading shareKey
162 | val currentUrl = window.location.href
163 | val shareKey = if (currentUrl.contains("#")) {
164 | currentUrl.substring(currentUrl.lastIndexOf("#") + 1).trim()
165 | } else {
166 | null
167 | }
168 | println("QuickTag: HomeViewModel:: shareKey: '$shareKey'")
169 | if (!shareKey.isNullOrBlank()) {
170 | // Load input for the shareKey
171 | googleSheetRepo.getSharedInput(
172 | shareKey = shareKey,
173 | onSharedInput = { sharedInput ->
174 | form = form.copy(data = sharedInput, isLoading = false)
175 | onFormChanged(form)
176 | sharedUrl = window.location.href
177 | },
178 | onFailed = { message ->
179 | window.alert(message)
180 | loadDefaultForm()
181 | }
182 | )
183 | } else {
184 | loadDefaultForm()
185 | }
186 | }
187 |
188 | private fun loadDefaultForm() {
189 | form = (formRepo.getFormData() ?: form.copy(data = DefaultValues.form)).copy(isLoading = false)
190 | }
191 |
192 | private fun refreshBenchmarks() {
193 | savedBenchmarks = benchmarkRepo.getSavedBenchmarks()
194 | }
195 |
196 | // Normal fields
197 | private val fullBenchmarkResults = mutableListOf()
198 |
199 |
200 | var timeoutId: Int? = null
201 | fun debounce(func: () -> Unit, delay: Int) {
202 | timeoutId?.let { clearTimeout(it) }
203 | timeoutId = setTimeout({
204 | func()
205 | }, delay)
206 | }
207 |
208 | fun onFormChanged(unfilteredForm: FormData, shouldSelectUnsaved: Boolean = true) {
209 | val oldFormData = form.data
210 |
211 | // filtering android log
212 | form = unfilteredForm.copy(data = filterOutAndroidJunkLog(unfilteredForm.data))
213 |
214 | // check if input changes
215 | if (oldFormData != form.data) {
216 | console.log("input has changed...")
217 | sharedUrl = null
218 | }
219 |
220 | formRepo.storeFormData(form)
221 |
222 | debounce(
223 | func = {
224 |
225 | this.shouldSelectUnsaved = shouldSelectUnsaved
226 | try {
227 | // clearing old data
228 | fullBenchmarkResults.clear()
229 | testNames.clear()
230 | focusGroups.clear()
231 | blockNames.clear()
232 | sdNodes.clear()
233 |
234 | // refill
235 | val (inputType, benchmarkResults, focusGroups) = BenchmarkResult.parse(form, currentFocusedGroup)
236 | ?: run {
237 | println("failed to parse form")
238 | reset()
239 | errorMsg = ""
240 | return@debounce
241 | }
242 | this.inputType = inputType
243 | fullBenchmarkResults.addAll(benchmarkResults)
244 | this.focusGroups.addAll(focusGroups)
245 |
246 |
247 | if (!focusGroups.contains(currentFocusedGroup)) {
248 | currentFocusedGroup = FOCUS_GROUP_ALL
249 | }
250 |
251 | if (currentFocusedGroup == FOCUS_GROUP_ALL) {
252 | oldAvgOfCount = -1
253 | } else if (oldAvgOfCount == -1) {
254 | oldAvgOfCount = avgOfCount
255 | }
256 |
257 | avgOfCount = benchmarkResults
258 | .flatMap {
259 | it.blockRows.map { blockRow ->
260 | blockRow.fullData.map { fullData ->
261 | fullData.value.size
262 | }
263 | }
264 | }.flatten().takeIf { it.isNotEmpty() }?.min() ?: -1
265 |
266 |
267 | when (inputType) {
268 | InputType.GENERIC -> {
269 | val newCharts = fullBenchmarkResults.toGenericChart()
270 | chartsBundle = newCharts
271 | onChartsBundleUpdated(newCharts)
272 | unit = ""
273 | }
274 |
275 | InputType.MACRO_BENCHMARK -> {
276 |
277 | testNames.addAll(fullBenchmarkResults.mapNotNull { it.testName }.toSet())
278 |
279 | val currentTestName = testNames.find { it == currentTestName } ?: testNames.firstOrNull()
280 | val filteredBenchmarkResult = if (currentTestName != null) {
281 | fullBenchmarkResults.filter { it.testName == currentTestName }
282 | } else {
283 | fullBenchmarkResults
284 | }
285 | val newCharts = filteredBenchmarkResult.toCharts()
286 | chartsBundle = newCharts
287 | onChartsBundleUpdated(newCharts)
288 | unit = "ms"
289 | }
290 | }
291 |
292 | if (currentFocusedGroup != FOCUS_GROUP_ALL) {
293 | fullBenchmarkResults
294 | .flatMap { it.blockRows }
295 | .forEach { blockRow ->
296 | console.log("Block row is ", blockRow)
297 | val population = blockRow.avgData.values
298 | val confidenceIntervals = population.calculateErrorMargins()
299 | sdNodes.add(
300 | SDNode(
301 | name = blockRow.title,
302 | population = population.toList(),
303 | standardDeviation = confidenceIntervals.standardDeviation.formatTwoDecimals(),
304 | errorMargin = mapOf(
305 | "68.3%" to confidenceIntervals.percentageMarginOf68p3.formatTwoDecimals(),
306 | "90%" to confidenceIntervals.percentageMarginOf90.formatTwoDecimals(),
307 | "95%" to confidenceIntervals.percentageMarginOf95.formatTwoDecimals(),
308 | "99%" to confidenceIntervals.percentageMarginOf99.formatTwoDecimals(),
309 | ),
310 | min = population.minOrNull() ?: 0f,
311 | median = population.average().toFloat().formatTwoDecimals(),
312 | max = population.maxOrNull() ?: 0f,
313 | percentiles = mapOf(
314 | "50%" to population.sorted()[min(0.50 * population.size, population.size - 1f.toDouble()).toInt()].formatTwoDecimals(),
315 | "90%" to population.sorted()[min(0.90 * population.size, population.size - 1f.toDouble()).toInt()].formatTwoDecimals(),
316 | "99%" to population.sorted()[min(0.99 * population.size, population.size - 1f.toDouble()).toInt()].formatTwoDecimals()
317 | )
318 | )
319 | )
320 | }
321 | }
322 |
323 |
324 | val autoGroupMapSize = chartsBundle?.groupMap?.autoGroupMap?.size ?: 0
325 | val wordColorMapSize = chartsBundle?.groupMap?.wordColorMap?.size ?: 0
326 | isAutoGroupButtonVisible = autoGroupMapSize != wordColorMapSize
327 | errorMsg = ""
328 | } catch (e: Throwable) {
329 | e.printStackTrace()
330 | errorMsg = e.message ?: ERROR_GENERIC
331 | reset()
332 | }
333 | },
334 | 300
335 | )
336 | }
337 |
338 |
339 | private fun Float.formatTwoDecimals(): Float {
340 | return asDynamic().toFixed(2).toString().toFloat()
341 | }
342 |
343 |
344 | // timestamp eg : 2024-06-29 11:30:46.641
345 | val fullTimestampRegex = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}".toRegex()
346 |
347 | // compact timestamp eg: 11:30:46.865
348 | val compactTimestampRegex = "\\d{2}:\\d{2}:\\d{2}\\.\\d{3}".toRegex()
349 |
350 | val logLevelRegex = "^(I|D|E|W|V) ".toRegex()
351 |
352 | /**
353 | * this is a custom logic to filter out android junk logs (personal)
354 | */
355 | private fun filterOutAndroidJunkLog(data: String): String {
356 | return data.split("\n")
357 | .filterNot { line ->
358 | // line removal
359 | line.contains("PROCESS ENDED", ignoreCase = false) ||
360 | line.contains("PROCESS STARTED", ignoreCase = false)
361 | }.joinToString(separator = "\n") {
362 | // line manipulation
363 | var line = it.replace(fullTimestampRegex, "").trimStart()
364 | line = line.replace(compactTimestampRegex, "").trimStart()
365 | if (line.startsWith("System.out ")) {
366 | line = line.replace("System.out ", "").trimStart()
367 | }
368 | line = line.replace(logLevelRegex, "").trimStart()
369 | line = when {
370 | line.contains("startup type is: cold") -> {
371 | "startup type is: cold"
372 | }
373 |
374 | line.contains("startup type is: warm") -> {
375 | "startup type is: warm"
376 | }
377 |
378 | line.contains("startup type is: hot") -> {
379 | "startup type is: hot"
380 | }
381 |
382 | else -> {
383 | line
384 | }
385 | }.trimStart()
386 | line
387 | }
388 | }
389 |
390 | private fun reset() {
391 | selectedBlockNameOne = null
392 | selectedBlockNameTwo = null
393 | blockNames.clear()
394 | chartsBundle = null
395 | summaries.clear()
396 | oldSummaries.clear()
397 | bestAggSummary = null
398 | worstAggSummary = null
399 | avgOfCount = -1
400 | oldAvgOfCount = -1
401 | sdNodes.clear()
402 | updateSummary()
403 | }
404 |
405 | private fun calcAggSummary() {
406 | val isGeneric = inputType == InputType.GENERIC
407 | val newAggSums = mutableListOf()
408 | for (blockNameOuter in blockNames) {
409 | for (blockNameInner in blockNames) {
410 | if (blockNameOuter == blockNameInner) {
411 | continue
412 | }
413 | chartsBundle?.charts?.mapNotNull { chart ->
414 | SummaryUtils.getSummaryOrThrow(
415 | currentFocusedGroup = currentFocusedGroup,
416 | isGeneric = isGeneric,
417 | chart = chart,
418 | selectedBlockNameOne = blockNameOuter,
419 | selectedBlockNameTwo = blockNameInner
420 | )
421 | }?.let { summaries ->
422 | var greenSum = 0
423 | var redSum = 0
424 | for (summary in summaries) {
425 | for (node in summary.nodes) {
426 | when {
427 | node.diff > 0 -> {
428 | // bad
429 | redSum += node.diff.toInt()
430 | }
431 |
432 | node.diff < 0 -> {
433 | // green
434 | greenSum -= node.diff.toInt()
435 | }
436 | }
437 | }
438 | }
439 | newAggSums.add(AggSummary(blockNameOuter, blockNameInner, sumOfGreen = greenSum, sumOfRed = redSum))
440 | }
441 | }
442 | }
443 |
444 | bestAggSummary = newAggSums.maxByOrNull { it.sumOfGreen }
445 | worstAggSummary = newAggSums.maxByOrNull { it.sumOfRed }
446 | }
447 |
448 | private fun onChartsBundleUpdated(chartsBundle: ChartsBundle) {
449 | blockNames.clear()
450 | val blockNames = chartsBundle.groupMap.wordColorMap.keys.toList()
451 | this.blockNames.addAll(blockNames)
452 | if (blockNames.size >= 2) {
453 | selectedBlockNameOne = blockNames[0]
454 | selectedBlockNameTwo = blockNames[1]
455 | } else {
456 | selectedBlockNameOne = null
457 | selectedBlockNameTwo = null
458 | }
459 | updateSummary()
460 | }
461 |
462 | private fun updateSummary() {
463 | if (currentFocusedGroup != FOCUS_GROUP_ALL && oldSummaries.isEmpty()) {
464 | // preserving previous summary because user is now focusing ona particular group
465 | oldSummaries.addAll(summaries)
466 | println("QuickTag: HomeViewModel:updateSummary: preserving ${summaries.size} summary nodes (old $oldAvgOfCount) ")
467 | }
468 |
469 | if (currentFocusedGroup == FOCUS_GROUP_ALL && oldSummaries.isNotEmpty()) {
470 | println("QuickTag: HomeViewModel:updateSummary: clearing ${oldSummaries.size} nodes")
471 | // user is not focused on a particular metric, hence two summaries are not needed.
472 | // the old summaries can now be cleared
473 | oldSummaries.clear()
474 | oldAvgOfCount = -1
475 | }
476 |
477 | // Calculating duration summary
478 | summaries.clear()
479 |
480 | val isGeneric = inputType == InputType.GENERIC
481 | val allSummaries = chartsBundle?.charts?.mapNotNull { chart ->
482 | SummaryUtils.getSummaryOrThrow(
483 | currentFocusedGroup = currentFocusedGroup,
484 | isGeneric = isGeneric,
485 | chart = chart,
486 | selectedBlockNameOne = selectedBlockNameOne,
487 | selectedBlockNameTwo = selectedBlockNameTwo
488 | )
489 | }
490 | summaries.addAll(allSummaries ?: emptyList())
491 | calcAggSummary()
492 | }
493 |
494 | fun onTestNameSelected(newTestName: String) {
495 | try {
496 | currentTestName = newTestName
497 | val filteredBenchmarkResult = if (currentTestName != null) {
498 | fullBenchmarkResults.filter { it.testName == currentTestName }
499 | } else {
500 | fullBenchmarkResults
501 | }
502 | val newCharts = filteredBenchmarkResult.toCharts()
503 | chartsBundle = newCharts
504 | updateSummary()
505 | errorMsg = ""
506 | } catch (e: Throwable) {
507 | summaries.clear()
508 | e.printStackTrace()
509 | errorMsg = e.message ?: ERROR_GENERIC
510 | }
511 | }
512 |
513 | fun onFocusGroupSelected(focusGroup: String) {
514 | currentFocusedGroup = focusGroup
515 | onFormChanged(form)
516 | }
517 |
518 | fun onTitleDoubleClicked() {
519 | isEditableTitleEnabled = true
520 | }
521 |
522 | fun onToggleAutoGroupClicked() {
523 | onFormChanged(form.copy(isAutoGroupEnabled = !form.isAutoGroupEnabled))
524 | }
525 |
526 | fun onToggleTestNameDetectionClicked() {
527 | onFormChanged(form.copy(isTestNameDetectionEnabled = !form.isTestNameDetectionEnabled))
528 | }
529 |
530 | fun onSaveClicked(formData: FormData) {
531 | val bName = window.prompt("Name: ")
532 | if (bName.isNullOrBlank()) {
533 | return
534 | }
535 |
536 | val isExist = savedBenchmarks.find { it.key == bName } != null
537 | if (isExist) {
538 | window.alert("Bruhh.. $bName exists! Try something else")
539 | return
540 | }
541 |
542 | // Appending new benchmark
543 | val newList = savedBenchmarks.toMutableList().apply {
544 | add(
545 | index = 0,
546 | element = SavedBenchmarkNode(
547 | key = bName, value = formData.data
548 | )
549 | )
550 | }
551 | benchmarkRepo.saveBenchmarks(newList)
552 | shouldSelectUnsaved = false
553 | refreshBenchmarks()
554 | }
555 |
556 | fun onShareClicked(formData: FormData) {
557 | if (sharedUrl != null) {
558 | // show the modal again
559 | showSharedModal()
560 | return
561 | }
562 |
563 | val startTime = Date().getTime()
564 | val isAwareDataPublic = userRepo.isAwareShareIsPublic()
565 | println("QuickTag: HomeViewModel:onShareClicked: isAwareDataPublic $isAwareDataPublic")
566 | if (isAwareDataPublic) {
567 | form = form.copy(isLoading = true)
568 | debounce(
569 | func = {
570 | // We need to split the input into chunk of 30,000 character
571 | val chunks = formData.data.chunked(30000)
572 | // since we're using the millis as Random see 10 should be enough 🤔
573 | val shareKey =
574 | "${RandomString.getRandomString(10)}_${Date().getTime()}_${RandomString.getRandomString(10)}"
575 |
576 | // Submit the Google form to insert the data to google sheet
577 | for ((index, chunk) in chunks.withIndex()) {
578 | try {
579 | googleFormRepo.insert(
580 | shareKey,
581 | index,
582 | chunk
583 | )
584 | } catch (e: Throwable) {
585 | e.printStackTrace()
586 | // ignoring
587 | }
588 | }
589 |
590 | // show a success message to user that the URL has been copied to the clipboard
591 | println("QuickTag: HomeViewModel:onShareClicked: Huhhaaa!!! shareKey: $shareKey. Checking data integrity...")
592 |
593 | // using shareKey and chunkSize to verify the upload
594 | retriedCount = 0;
595 | window.setTimeout({
596 | confirmChunkSize(shareKey, chunks, startTime)
597 | },1500)
598 |
599 | },
600 | delay = 500
601 | )
602 | } else {
603 | js("var myModal = new bootstrap.Modal(document.getElementById('shareAwareModal'), {});myModal.show();")
604 | }
605 | }
606 |
607 | fun showSharedModal() {
608 | js("var myModal = new bootstrap.Modal(document.getElementById('sharedModal'), {});myModal.show();")
609 | }
610 |
611 | private var retriedCount = 0
612 | private fun confirmChunkSize(
613 | shareKey: String,
614 | chunks: List,
615 | startTime: Double,
616 | ) {
617 | retriedCount++
618 | googleSheetRepo.getChunkSize(
619 | shareKey = shareKey,
620 | onChunkSize = { remoteChunkSize ->
621 | println("QuickTag: HomeViewModel:confirmChunkSize: remote chunk size is $remoteChunkSize (expected ${chunks.size})")
622 | if (remoteChunkSize == chunks.size) {
623 | // Data integrity ✅
624 | println("QuickTag: HomeViewModel:onShareClicked: SHARE SUCCESS!")
625 | println("QuickTag: HomeViewModel:onShareClicked: time took : ${Date().getTime() - startTime}ms")
626 | form = form.copy(isLoading = false)
627 | /*window.prompt(
628 | message = "Ready to share, copy below URL",
629 | default =
630 | )*/
631 | sharedUrl = "${window.location.origin}/benchart/#$shareKey"
632 | showSharedModal()
633 | } else {
634 | if (retriedCount >= RETRY_COUNT) {
635 | form = form.copy(isLoading = false)
636 | window.alert("Share failed. Expected ${chunks.size} chunk(s) but found $remoteChunkSize")
637 | } else {
638 | retryGetChunkSize(shareKey, chunks, startTime)
639 | }
640 | }
641 | },
642 | onFailed = { reason ->
643 | println("QuickTag: HomeViewModel:confirmChunkSize: failed: $reason : retried: $retriedCount/ $RETRY_COUNT")
644 | if (retriedCount >= RETRY_COUNT) {
645 | form = form.copy(isLoading = false)
646 | window.alert("Share failed : $reason")
647 | } else {
648 | retryGetChunkSize(shareKey, chunks, startTime)
649 | }
650 | }
651 | )
652 | }
653 |
654 | private fun retryGetChunkSize(
655 | shareKey: String,
656 | chunks: List,
657 | startTime: Double
658 | ) {
659 | setTimeout(
660 | {
661 | confirmChunkSize(shareKey, chunks, startTime)
662 | },
663 | 2000
664 | )
665 | }
666 |
667 | fun onLoadBenchmarkClicked(savedBenchmarkNode: SavedBenchmarkNode) {
668 | val newForm = form.copy(data = savedBenchmarkNode.value)
669 | onFormChanged(newForm, shouldSelectUnsaved = false)
670 | }
671 |
672 | fun onDeleteBenchmarkClicked(deletedBenchmarkNode: SavedBenchmarkNode) {
673 | val isYes = window.confirm(
674 | "Do you want to delete `${deletedBenchmarkNode.key}` ?"
675 | )
676 |
677 | if (isYes) {
678 | benchmarkRepo.delete(deletedBenchmarkNode)
679 | shouldSelectUnsaved = true
680 | refreshBenchmarks()
681 | }
682 | }
683 |
684 | fun onSavedBenchmarkChanged(key: String) {
685 | shouldSelectUnsaved = key == KEY_UNSAVED_BENCHMARK
686 | if (shouldSelectUnsaved) {
687 | val newForm = formRepo.getFormData() ?: form
688 | onFormChanged(newForm, shouldSelectUnsaved = false)
689 | }
690 | }
691 |
692 | fun onBlockNameOneChanged(newBlockName: String) {
693 | selectedBlockNameOne = newBlockName
694 | updateSummary()
695 | }
696 |
697 | fun onBlockNameTwoChanged(newBlockName: String) {
698 | selectedBlockNameTwo = newBlockName
699 | updateSummary()
700 | }
701 |
702 | fun onBestClicked() {
703 | selectedBlockNameOne = bestAggSummary?.blockOneName
704 | selectedBlockNameTwo = bestAggSummary?.blockTwoName
705 | updateSummary()
706 | }
707 |
708 | fun onWorstClicked() {
709 | selectedBlockNameOne = worstAggSummary?.blockOneName
710 | selectedBlockNameTwo = worstAggSummary?.blockTwoName
711 | updateSummary()
712 | }
713 |
714 | fun onDotClicked(focusGroup: String) {
715 | if (focusGroups.contains(focusGroup)) {
716 | onFocusGroupSelected(focusGroup)
717 | }
718 | }
719 |
720 |
721 | fun onAwarePublicShare() {
722 | userRepo.setAwareShareIsPublic(isAware = true)
723 | onShareClicked(form)
724 | }
725 |
726 | fun onCopyToClipboardClicked(sharedUrl: String?) {
727 | if (sharedUrl != null) {
728 | window.navigator.clipboard.writeText(sharedUrl)
729 | .then(
730 | onFulfilled = {
731 | console.log("Copied to clipboard")
732 | },
733 | onRejected = {
734 | window.alert("Failed to copy to clipboard : ${it.message}")
735 | }
736 | )
737 | } else {
738 | window.alert("Failed to copy to clipboard. data is null")
739 | }
740 | }
741 |
742 | }
743 |
744 | data class AggSummary(
745 | val blockOneName: String,
746 | val blockTwoName: String,
747 | val sumOfGreen: Int,
748 | val sumOfRed: Int
749 | )
750 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/page/home/ShareAwareModal.kt:
--------------------------------------------------------------------------------
1 | package page.home
2 |
3 | import androidx.compose.runtime.Composable
4 | import kotlinx.browser.window
5 | import org.jetbrains.compose.web.attributes.ButtonType
6 | import org.jetbrains.compose.web.attributes.type
7 | import org.jetbrains.compose.web.css.marginTop
8 | import org.jetbrains.compose.web.css.px
9 | import org.jetbrains.compose.web.dom.Button
10 | import org.jetbrains.compose.web.dom.Div
11 | import org.jetbrains.compose.web.dom.H4
12 | import org.jetbrains.compose.web.dom.P
13 | import org.jetbrains.compose.web.dom.Text
14 |
15 | @Composable
16 | fun ShareAwareModal(
17 | onShareClicked : () -> Unit
18 | ){
19 | Div(
20 | attrs = {
21 | id("shareAwareModal")
22 | classes("modal", "fade")
23 | }
24 | ) {
25 | Div(
26 | attrs = {
27 | classes("modal-dialog", "modal-lg")
28 | }
29 | ) {
30 | Div(
31 | attrs = {
32 | classes("modal-content")
33 | }
34 | ) {
35 | Div(
36 | attrs = {
37 | classes("modal-header")
38 | }
39 | ) {
40 | H4(
41 | attrs = {
42 | classes("modal-title")
43 | }
44 | ) {
45 | Text("Share")
46 | }
47 | }
48 |
49 | Div(
50 | attrs = {
51 | classes("modal-body")
52 | }
53 | ) {
54 | P {
55 | Text("""
56 | Ahh..it looks like you're using the 'Share' feature for the first time.
57 | Please be aware that the data you share will be visible to everyone.
58 | Make sure your input doesn't contain any sensitive data.
59 |
60 | If you need private share, please vote for the feature below :)
61 | """.trimIndent())
62 | }
63 | }
64 |
65 | Div(
66 | attrs = {
67 | classes("modal-footer")
68 | }
69 | ) {
70 |
71 | Button(
72 | attrs = {
73 | classes("btn", "btn-dark")
74 | style {
75 | marginTop(10.px)
76 | }
77 |
78 | onClick {
79 | window.open("https://forms.gle/KtPAA5LMeE8sak5h9", target = "_blank")
80 | }
81 | type(ButtonType.Button)
82 | }
83 | ) {
84 | Text("Vote for Private Share")
85 | }
86 |
87 | Button(
88 | attrs = {
89 | classes("btn", "btn-danger")
90 | attr("data-bs-dismiss", "modal")
91 | style {
92 | marginTop(10.px)
93 | }
94 | type(ButtonType.Button)
95 | }
96 | ) {
97 | Text("Cancel Share")
98 | }
99 |
100 | Button(
101 | attrs = {
102 | classes("btn", "btn-success")
103 | attr("data-bs-dismiss", "modal")
104 | style {
105 | marginTop(10.px)
106 | }
107 |
108 | onClick {
109 | onShareClicked()
110 | }
111 | type(ButtonType.Button)
112 | }
113 | ) {
114 | Text("Understood, Share!")
115 | }
116 | }
117 | }
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/page/home/SharedModal.kt:
--------------------------------------------------------------------------------
1 | package page.home
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import components.Strong
6 | import org.jetbrains.compose.web.attributes.ButtonType
7 | import org.jetbrains.compose.web.attributes.type
8 | import org.jetbrains.compose.web.css.marginTop
9 | import org.jetbrains.compose.web.css.px
10 | import org.jetbrains.compose.web.dom.Button
11 | import org.jetbrains.compose.web.dom.Div
12 | import org.jetbrains.compose.web.dom.H4
13 | import org.jetbrains.compose.web.dom.Text
14 |
15 | @Composable
16 | fun SharedModal(
17 | shareUrl : String?,
18 | onCopyToClipboardClicked : (shareUrl : String?) -> Unit
19 | ){
20 | Div(
21 | attrs = {
22 | id("sharedModal")
23 | classes("modal", "fade")
24 | }
25 | ) {
26 | Div(
27 | attrs = {
28 | classes("modal-dialog", "modal-lg")
29 | }
30 | ) {
31 | Div(
32 | attrs = {
33 | classes("modal-content")
34 | }
35 | ) {
36 | Div(
37 | attrs = {
38 | classes("modal-header")
39 | }
40 | ) {
41 | H4(
42 | attrs = {
43 | classes("modal-title")
44 | }
45 | ) {
46 | Text("🚀 Share URL Ready!")
47 | }
48 | }
49 |
50 | Div(
51 | attrs = {
52 | classes("modal-body")
53 | }
54 | ) {
55 | Div(
56 | attrs = {
57 | classes("alert","alert-success")
58 | }
59 | ) {
60 | Strong {
61 | if(shareUrl!=null){
62 | Text(shareUrl)
63 | }
64 | }
65 | }
66 | }
67 |
68 | Div(
69 | attrs = {
70 | classes("modal-footer")
71 | }
72 | ) {
73 |
74 | Button(
75 | attrs = {
76 | classes("btn", "btn-success")
77 | attr("data-bs-dismiss", "modal")
78 | style {
79 | marginTop(10.px)
80 | }
81 |
82 | onClick {
83 | onCopyToClipboardClicked(shareUrl)
84 | }
85 | type(ButtonType.Button)
86 | }
87 | ) {
88 | Text("Copy to clipboard")
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/repo/BenchmarkRepo.kt:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import components.SavedBenchmarkNode
4 | import components.SavedBenchmarks
5 | import kotlinx.browser.window
6 | import kotlinx.serialization.decodeFromString
7 | import kotlinx.serialization.encodeToString
8 | import utils.JsonUtils
9 |
10 | interface BenchmarkRepo {
11 | fun getSavedBenchmarks(): List
12 | fun saveBenchmarks(newList: List)
13 | fun delete(deletedBenchmarkNode: SavedBenchmarkNode)
14 | }
15 |
16 | class BenchmarkRepoImpl : BenchmarkRepo {
17 |
18 | companion object {
19 | private const val KEY_SAVED_BENCHMARKS = "savedBenchmarks"
20 | }
21 |
22 |
23 | override fun getSavedBenchmarks(): List {
24 | val savedBenchmarksString = window.localStorage.getItem(KEY_SAVED_BENCHMARKS)
25 | val savedBenchmark = if (savedBenchmarksString == null) {
26 | // Creating first saved benchmark
27 | SavedBenchmarks(items = listOf())
28 | } else {
29 | println("JSON is '$savedBenchmarksString'")
30 | try {
31 | JsonUtils.json.decodeFromString(savedBenchmarksString)
32 | }catch (e: Exception){
33 | e.printStackTrace()
34 | saveBenchmarks(listOf()) // reset
35 | SavedBenchmarks(items = listOf())
36 | }
37 | }
38 |
39 | return savedBenchmark.items.toList()
40 | }
41 |
42 | override fun saveBenchmarks(newList: List) {
43 | val savedBenchmarks = JsonUtils.json.encodeToString(SavedBenchmarks(newList))
44 | window.localStorage.setItem(KEY_SAVED_BENCHMARKS, savedBenchmarks)
45 | }
46 |
47 | override fun delete(deletedBenchmarkNode: SavedBenchmarkNode) {
48 | // Appending new benchmark
49 | val newList = getSavedBenchmarks().toMutableList().apply {
50 | removeAll { it.key == deletedBenchmarkNode.key }
51 | }
52 | saveBenchmarks(newList)
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/repo/FormRepo.kt:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import kotlinx.browser.window
4 | import model.FormData
5 |
6 | interface FormRepo {
7 | fun storeFormData(newForm: FormData)
8 | fun getFormData(): FormData?
9 | }
10 |
11 | class FormRepoImpl : FormRepo {
12 | companion object {
13 | private const val KEY_AUTO_FORM_INPUT = "auto_form_input"
14 | private const val KEY_IS_TEST_NAME_DETECTION_ENABLED = "is_test_name_detection_enabled"
15 | private const val KEY_IS_AUTO_GROUP_ENABLED = "is_auto_group_enabled"
16 | }
17 |
18 | override fun storeFormData(newForm: FormData) {
19 | window.localStorage.apply {
20 | setItem(KEY_AUTO_FORM_INPUT, newForm.data)
21 | setItem(KEY_IS_TEST_NAME_DETECTION_ENABLED, newForm.isTestNameDetectionEnabled.toString())
22 | setItem(KEY_IS_AUTO_GROUP_ENABLED, newForm.isAutoGroupEnabled.toString())
23 | }
24 | }
25 |
26 | override fun getFormData(): FormData? {
27 | val localStorage = window.localStorage
28 | val data = localStorage.getItem(KEY_AUTO_FORM_INPUT) ?: return null
29 | val isTestNameDetectionEnabled = localStorage.getItem(KEY_IS_TEST_NAME_DETECTION_ENABLED).toBoolean()
30 | val isAutoGroupEnabled = localStorage.getItem(KEY_IS_AUTO_GROUP_ENABLED).toBoolean()
31 | return FormData(data, isTestNameDetectionEnabled, isAutoGroupEnabled, isLoading = true) // true because its not reached UI yet
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/repo/GoogleFormRepo.kt:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import org.w3c.xhr.XMLHttpRequest
4 |
5 | interface GoogleFormRepo {
6 | fun insert(
7 | shareKey : String,
8 | chunkIndex : Int,
9 | inputChunk : String
10 | )
11 | }
12 |
13 | class GoogleFormRepoImpl : GoogleFormRepo {
14 |
15 | companion object {
16 | private const val FORM_SUBMISSION_URL =
17 | "https://docs.google.com/forms/d/e/1FAIpQLSfYy0ZnzlSot_3SpJ7GVK9umEpf3Dqzz1pQ7jyLUVd7jO2qCQ/formResponse"
18 | }
19 |
20 | override fun insert(shareKey: String, chunkIndex: Int, inputChunk: String) {
21 | val data = "entry.1218983684=$shareKey&entry.1886726465=$chunkIndex&entry.1340578003=$inputChunk";
22 | val xhr = XMLHttpRequest()
23 | xhr.open("POST", FORM_SUBMISSION_URL, async = false)
24 | xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
25 | xhr.send(data)
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/repo/GoogleSheetRepo.kt:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import org.w3c.xhr.XMLHttpRequest
4 |
5 | interface GoogleSheetRepo {
6 | fun getChunkSize(
7 | shareKey: String,
8 | onChunkSize: (chunkSize: Int) -> Unit,
9 | onFailed: (reason: String) -> Unit
10 | )
11 |
12 | fun getSharedInput(
13 | shareKey: String,
14 | onSharedInput: (input: String) -> Unit,
15 | onFailed: (reason: String) -> Unit
16 | )
17 | }
18 |
19 | class GoogleSheetRepoImpl : GoogleSheetRepo {
20 | companion object {
21 | private const val BASE_URL =
22 | "https://docs.google.com/spreadsheets/d/1U1bKMHN0hlpZ1CVke3TB3-Xc20ZJwZxlMWYXpMcII-k/gviz/tq?tqx=out:csv&sheet=Sheet1"
23 | }
24 |
25 | override fun getChunkSize(
26 | shareKey: String,
27 | onChunkSize: (chunkSize: Int) -> Unit,
28 | onFailed: (reason: String) -> Unit
29 | ) {
30 | try {
31 | val chunkCountUrl = "$BASE_URL&tq=SELECT COUNT(C) WHERE B = '$shareKey'"
32 | val xhr = XMLHttpRequest()
33 | xhr.open("GET", chunkCountUrl)
34 | xhr.onreadystatechange = { _ ->
35 | println("QuickTag: GoogleSheetRepoImpl:getChunkCount: readyState: ${xhr.readyState}, status = ${xhr.status}")
36 | if (xhr.readyState == 4.toShort()) {
37 | if (xhr.status == 200.toShort()) {
38 | val responseLines = xhr.responseText.split("\n")
39 | if (responseLines.size == 2) {
40 | // chunk exist
41 | val chunkSize = responseLines[1].replace("\"", "").toInt()
42 | println("QuickTag: GoogleSheetRepoImpl:getChunkSize: chunk size is '$chunkSize'")
43 | onChunkSize(chunkSize)
44 | } else {
45 | // share doesn't exist
46 | onFailed("No chunk exist for shareKey '$shareKey'")
47 | }
48 | } else {
49 | onFailed("Share request failed")
50 | }
51 | }
52 | }
53 | xhr.send()
54 | } catch (e: Throwable) {
55 | e.printStackTrace()
56 | onFailed(e.message ?: "Something wrong")
57 | }
58 | }
59 |
60 | override fun getSharedInput(
61 | shareKey: String,
62 | onSharedInput: (input: String) -> Unit,
63 | onFailed: (reason: String) -> Unit
64 | ) {
65 | try {
66 | val chunkCountUrl = "$BASE_URL&tq=SELECT C,D WHERE B = '$shareKey' ORDER BY C"
67 | val xhr = XMLHttpRequest()
68 | xhr.open("GET", chunkCountUrl)
69 | xhr.onreadystatechange = { _ ->
70 | println("QuickTag: GoogleSheetRepoImpl:getChunkCount: readyState: ${xhr.readyState}, status = ${xhr.status}")
71 | if (xhr.readyState == 4.toShort()) {
72 | if (xhr.status == 200.toShort()) {
73 | val responseLines = xhr.responseText
74 | val firstLineBreakIndex = responseLines.indexOf('\n')
75 | if (firstLineBreakIndex != -1) {
76 | val sharedInput = responseLines
77 | .substring(firstLineBreakIndex+1, responseLines.length - 1)
78 | .replace("\"\\n\"(?:\\d+)\",\"".toRegex(),"")
79 | .substring(5)
80 | onSharedInput(sharedInput)
81 | } else {
82 | onFailed("Invalid shareKey '$shareKey'")
83 | }
84 | } else {
85 | onFailed("Share request failed")
86 | }
87 | }
88 | }
89 | xhr.send()
90 | } catch (e: Throwable) {
91 | onFailed(e.message ?: "Something wrong")
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/repo/UserRepo.kt:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import kotlinx.browser.window
4 |
5 | interface UserRepo {
6 | fun isAwareShareIsPublic() : Boolean
7 | fun setAwareShareIsPublic(isAware : Boolean)
8 | }
9 |
10 | class UserRepoImpl : UserRepo {
11 | companion object{
12 | private const val KEY_IS_AWARE_SHARE_IS_PUBLIC = "is_aware_share_is_public"
13 | }
14 | override fun isAwareShareIsPublic(): Boolean {
15 | return window.localStorage.getItem(KEY_IS_AWARE_SHARE_IS_PUBLIC)?.toBoolean() ?: false
16 | }
17 |
18 | override fun setAwareShareIsPublic(isAware: Boolean) {
19 | window.localStorage.setItem(KEY_IS_AWARE_SHARE_IS_PUBLIC, isAware.toString())
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/utils/DefaultValues.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | object DefaultValues {
4 | val form = """
5 | - Before 1
6 | # first line will be treated as title of the block
7 | special chars will be stripped from the title
8 | HomeScrollBenchmark_scrollTest
9 | frameDurationCpuMs P50 40.5, P90 45.8, P95 60.4, P99 80.4
10 | frameOverrunMs P50 -5.9, P90 7.0, P95 20.1, P99 64.4
11 | Traces: Iteration 0 1 2 3 4
12 |
13 | ## Before 2
14 | # line breaks are used to separate the block
15 | HomeScrollBenchmark_scrollTest
16 | frameDurationCpuMs P50 45.5, P90 43.8, P95 58.4, P99 78.4
17 | frameOverrunMs P50 -6.5, P90 5.4, P95 15.0, P99 60.3
18 | Traces: Iteration 0 1 2 3 4
19 |
20 | After 1
21 | you can include whatever text you want anywhere you want
22 | HomeScrollBenchmark_scrollTest
23 | frameDurationCpuMs P50 13.6, P90 21.8, P95 27.5, P99 49.4
24 | the order doesn't matter
25 | frameOverrunMs P50 -6.2, P90 7.3, P95 19.5, P99 61.7
26 | Traces: Iteration 0 1 2 3 4
27 |
28 | > After 2
29 | HomeScrollBenchmark_scrollTest
30 | frameDurationCpuMs P50 13.8, P90 21.9, P95 27.3, P99 53.4
31 | see.. am some random text
32 | frameOverrunMs P50 -5.7, P90 7.4, P95 22.4, P99 63.2
33 | Traces: Iteration 0 1 2 3 4
34 | """.trimIndent()
35 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/utils/JsonUtils.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import kotlinx.serialization.json.Json
4 |
5 | object JsonUtils {
6 | val json = Json {
7 | ignoreUnknownKeys = true
8 | }
9 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/utils/Math.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import page.home.ConfidenceIntervals
4 | import kotlin.math.sqrt
5 |
6 |
7 |
8 | fun Collection.calculateErrorMargins(): ConfidenceIntervals {
9 | if (this.isEmpty()) {
10 | return ConfidenceIntervals(
11 | mean = 0f,
12 | marginOf68p3 = 0f,
13 | marginOf90 = 0f,
14 | marginOf95 = 0f,
15 | marginOf99 = 0f,
16 | percentageMarginOf68p3 = 0f,
17 | percentageMarginOf90 = 0f,
18 | percentageMarginOf95 = 0f,
19 | percentageMarginOf99 = 0f,
20 | sampleSize = 0,
21 | standardDeviation = 0f
22 | )
23 | }
24 |
25 | val mean = this.average().toFloat()
26 | val sampleSize = this.size
27 | val stdDev = this.populationStandardDeviation()
28 |
29 | val standardError = stdDev / sqrt(sampleSize.toFloat())
30 |
31 | // Calculate absolute margins of error for different confidence levels
32 | val margin68p3 = standardError // 68.3% confidence
33 | val margin90 = standardError * 1.645f // 90% confidence
34 | val margin95 = standardError * 1.96f // 95% confidence
35 | val margin99 = standardError * 2.576f // 99% confidence
36 |
37 | // Calculate percentage margins relative to mean
38 | // Avoid division by zero if mean is 0
39 | val percentMargin68p3 = if (mean != 0f) (margin68p3 / mean) * 100f else 0f
40 | val percentMargin90 = if (mean != 0f) (margin90 / mean) * 100f else 0f
41 | val percentMargin95 = if (mean != 0f) (margin95 / mean) * 100f else 0f
42 | val percentMargin99 = if (mean != 0f) (margin99 / mean) * 100f else 0f
43 |
44 | return ConfidenceIntervals(
45 | mean = mean,
46 | marginOf68p3 = margin68p3,
47 | marginOf90 = margin90,
48 | marginOf95 = margin95,
49 | marginOf99 = margin99,
50 | percentageMarginOf68p3 = percentMargin68p3,
51 | percentageMarginOf90 = percentMargin90,
52 | percentageMarginOf95 = percentMargin95,
53 | percentageMarginOf99 = percentMargin99,
54 | sampleSize = sampleSize,
55 | standardDeviation = stdDev
56 | )
57 | }
58 |
59 | private fun Collection.populationStandardDeviation(): Float {
60 | if (this.isEmpty()) return 0f
61 |
62 | val mean = this.average()
63 | val sumSquaredDiffs = this.sumOf {
64 | val diff = it - mean
65 | (diff * diff).toDouble()
66 | }
67 | val variance = sumSquaredDiffs / this.size
68 | return sqrt(variance).toFloat()
69 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/utils/RandomString.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 |
4 | import kotlin.js.Date
5 | import kotlin.random.Random
6 |
7 | /**
8 | * Created by theapache64 on 9/4/16.
9 | * and reused in 2024 :P
10 | */
11 | object RandomString {
12 | private const val RANDOM_ENGINE = "0123456789AaBbCcDdEeFfGgHhIiJjKkLkMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
13 |
14 | fun getRandomString(length: Int): String {
15 | val random = Random(Date().getTime().toInt() + (0..99999999999999999).random())
16 | val apiKeyBuilder = StringBuilder()
17 | for (i in 0 until length) {
18 | apiKeyBuilder.append(RANDOM_ENGINE[random.nextInt(RANDOM_ENGINE.length)])
19 | }
20 | return apiKeyBuilder.toString()
21 | }
22 | }
--------------------------------------------------------------------------------
/src/jsMain/kotlin/utils/SummaryUtils.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import components.Summary
4 | import components.SummaryNode
5 | import core.BenchmarkResult.Companion.FOCUS_GROUP_ALL
6 | import core.MetricUnit
7 | import core.getMetricEmoji
8 | import model.Chart
9 | import kotlin.math.absoluteValue
10 |
11 | object SummaryUtils {
12 |
13 | private fun getMetricTitle(label: String): String {
14 | return label
15 | }
16 |
17 | private val highIsGoodMetricRegex = arrayOf(
18 | "frameCount",
19 | "gfxFrameTotalCount",
20 | ).joinToString(separator = "|", prefix = "(", postfix = ")").toRegex()
21 |
22 | fun getSummaryOrThrow(
23 | currentFocusedGroup: String,
24 | isGeneric: Boolean,
25 | chart: Chart,
26 | selectedBlockNameOne: String?,
27 | selectedBlockNameTwo: String?,
28 | ): Summary? {
29 | if (selectedBlockNameOne == null || selectedBlockNameTwo == null) {
30 | println("blank block name detected. skipping summary")
31 | return null
32 | }
33 |
34 | val combinedMap = mutableMapOf>()
35 | val words = listOf(selectedBlockNameOne, selectedBlockNameTwo)
36 | println("words : $words")
37 | for (word in words) {
38 |
39 | combinedMap[word] =
40 | chart.dataSets.filterKeys { it.startsWith(word) }.values.map { it.values.toFloatArray() }
41 | .let { arrays ->
42 | // Sum
43 | val newArray = mutableListOf().apply {
44 | repeat(chart.dataSets.values.first().size) {
45 | add(0f)
46 | }
47 | }
48 | for (array in arrays) {
49 | for (i in newArray.indices) {
50 | newArray[i] = newArray[i] + array[i]
51 | }
52 | }
53 | // Average
54 | for (i in newArray.indices) {
55 | newArray[i] = newArray[i] / arrays.size
56 | }
57 | newArray
58 | }
59 | }
60 | println("combinedMap : ${combinedMap.map { it.value.toList() }}")
61 |
62 | val summaryNodes = mutableListOf()
63 | val segments = chart.dataSets.values.first().keys.toList()
64 | println("segments: $segments")
65 |
66 | val title = if (isGeneric) {
67 | if (currentFocusedGroup == FOCUS_GROUP_ALL) {
68 | "📊 $selectedBlockNameOne vs $selectedBlockNameTwo"
69 | } else {
70 | "📊 ${chart.label}"
71 | }
72 | } else {
73 | "${getMetricEmoji(chart.label)} ${getMetricTitle(chart.label)}"
74 | }
75 |
76 | repeat(segments.size) { index ->
77 | val segment = segments[index]
78 | val after = combinedMap[words[1]]?.get(index) ?: 0f
79 | val before = combinedMap[words[0]]?.get(index) ?: 0f
80 | println("before : '$before' -> after: '$after'")
81 | val diff = "${(after - before).asDynamic().toFixed(2)}".toFloat()
82 | var percDiff = if (before == 0f) {
83 | after * 100
84 | } else {
85 | (((before - after) / before) * 100)
86 | }
87 | percDiff = "${percDiff.asDynamic().toFixed(2)}".toFloat().absoluteValue as Float
88 |
89 | val isHighGoodMetric = highIsGoodMetricRegex.containsMatchIn(title)
90 |
91 | val resultWord = if (diff == 0f) {
92 | "equally"
93 | } else if (isHighGoodMetric == (diff > 0)) {
94 | "better"
95 | } else {
96 | "worse"
97 | }
98 | val symbol = if (diff > 0) "+" else ""
99 | val emoji = if (diff > 0 == isHighGoodMetric) "✅" else "❌"
100 | val badgeClass = when {
101 | diff == 0f -> "secondary"
102 | diff > 0 != isHighGoodMetric -> "danger"
103 | else -> "success"
104 | }
105 | summaryNodes.add(
106 | SummaryNode(
107 | isGeneric = isGeneric,
108 | emoji = emoji,
109 | segment = segment,
110 | label = words[1],
111 | percentage = percDiff,
112 | stateWord = resultWord,
113 | diff = diff,
114 | diffSymbol = symbol,
115 | after = "${after.asDynamic().toFixed(2)}".toFloat(),
116 | before = "${before.asDynamic().toFixed(2)}".toFloat(),
117 | unit = getMetricUnit(title),
118 | badgeClass = badgeClass,
119 | )
120 | )
121 | }
122 |
123 |
124 | return Summary(title = title, summaryNodes)
125 | }
126 |
127 | private fun getMetricUnit(title: String): MetricUnit? {
128 | return when {
129 | title.endsWith("Ms") -> MetricUnit.Ms
130 | title.endsWith("Mah") -> MetricUnit.Mah
131 | title.endsWith("Kb") -> MetricUnit.Kb
132 | title.endsWith("ViewCount") -> MetricUnit.View
133 | title.endsWith("Percent") -> MetricUnit.Percentage
134 | title.contains("frame", ignoreCase = true) && title.contains("count", ignoreCase = true) -> MetricUnit.Frame
135 | else -> null
136 | }
137 | }
138 | }
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following graphics from Twitter Twemoji:
2 |
3 | - Graphics Title: 1f4ca.svg
4 | - Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
5 | - Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f4ca.svg
6 | - Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
7 |
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/src/jsMain/resources/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/src/jsMain/resources/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/src/jsMain/resources/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/src/jsMain/resources/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/src/jsMain/resources/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/benchart/52236285b95d3e8195f5e0b52a143a83439cf5d9/src/jsMain/resources/icons/favicon.ico
--------------------------------------------------------------------------------
/src/jsMain/resources/icons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/src/jsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 | BenChart
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 |
36 |
--------------------------------------------------------------------------------
/src/jsTest/kotlin/GoogleFormRepoTest.kt:
--------------------------------------------------------------------------------
1 | import org.w3c.xhr.XMLHttpRequest
2 | import repo.GoogleFormRepoImpl
3 | import kotlin.test.Test
4 |
5 | class GoogleFormRepoTest {
6 |
7 |
8 | private val googleFormRepo = GoogleFormRepoImpl()
9 |
10 | @Test
11 | fun writeDataToSheet() {
12 | googleFormRepo.insert(
13 | shareKey = "myShareKey",
14 | chunkIndex = 0,
15 | inputChunk = "iamTheInput"
16 | )
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/jsTest/kotlin/GoogleSheetRepoTest.kt:
--------------------------------------------------------------------------------
1 | import org.w3c.xhr.XMLHttpRequest
2 | import repo.GoogleFormRepoImpl
3 | import kotlin.test.Test
4 |
5 | class GoogleSheetRepoTest {
6 |
7 | @Test
8 | fun readDataFromSheet(){
9 | val shareKey = "iamShareKey"
10 | val xhr = XMLHttpRequest()
11 | xhr.open("GET", getReadUrl(shareKey), async = false)
12 | // read data
13 | xhr.onload = {
14 | println("QuickTag: AppTest:readDataFromSheet: ${xhr.responseText}")
15 | }
16 | xhr.send()
17 | }
18 |
19 | private fun getReadUrl(shareKey : String): String {
20 | return "https://docs.google.com/spreadsheets/d/1U1bKMHN0hlpZ1CVke3TB3-Xc20ZJwZxlMWYXpMcII-k/gviz/tq?tqx=out:csv&sheet=Sheet1&tq=SELECT%20C,D%20WHERE%20B%20=%20'$shareKey'"
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/DemoDataGen.kt:
--------------------------------------------------------------------------------
1 | import java.util.*
2 |
3 | fun main() {
4 | startupTimeGen()
5 | }
6 |
7 | fun startupTimeGen() {
8 | val randomEng = 500..5000
9 | println("How many?: ")
10 | val count = Scanner(System.`in`).nextInt()
11 | repeat(count) {
12 | println(
13 | """
14 | timeToFullDisplayMs min ${randomEng.random()}, median ${randomEng.random()}, max ${randomEng.random()}
15 | timeToInitialDisplayMs min ${randomEng.random()}, median ${randomEng.random()}, max ${randomEng.random()}
16 | """.trimIndent()
17 | )
18 | println("-------------------------")
19 | }
20 | }
21 |
22 | fun frameTimeGen() {
23 | val randomEng = 0..100
24 | println("How many?: ")
25 | val count = Scanner(System.`in`).nextInt()
26 | repeat(count) {
27 | println(
28 | """
29 | frameDurationCpuMs P50 ${randomEng.random()}, P90 ${randomEng.random()}, P95 ${randomEng.random()}, P99 ${randomEng.random()}
30 | frameOverrunMs P50 ${randomEng.random()}, P90 ${randomEng.random()}, P95 ${randomEng.random()}, P99 ${randomEng.random()}
31 | """.trimIndent()
32 | )
33 | println("-------------------------")
34 | }
35 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/Test.kt:
--------------------------------------------------------------------------------
1 | fun main() {
2 | val x = arrayOf(
3 | "B"
4 | )
5 | x[0] = "AA"
6 | println(x.toList())
7 | }
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/core/TextNumberLineTest.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 |
7 | class TextNumberLineTest {
8 | @Test
9 | fun simple() {
10 | TextNumberLine.parse(0, "a = 10").also {
11 | assertEquals(TextNumberLine("a = ", 10f), it)
12 | }
13 | }
14 |
15 | @Test
16 | fun withUnit() {
17 | TextNumberLine.parse(0,"a = 10 rs").also {
18 | assertEquals(TextNumberLine("a = ", 10f), it)
19 | }
20 | }
21 |
22 | @Test
23 | fun numeric() {
24 | TextNumberLine.parse(0,"2019 = 20").also {
25 | assertEquals(TextNumberLine("2019 = ", 20f), it)
26 | }
27 | }
28 |
29 | }
--------------------------------------------------------------------------------